snap-python 0.1.0__tar.gz → 0.1.2__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.1.2/PKG-INFO +66 -0
- snap_python-0.1.2/README.md +44 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/pyproject.toml +5 -3
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/client.py +45 -33
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/snaps.py +98 -43
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/store.py +208 -5
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/info.py +53 -13
- snap_python-0.1.2/src/snap_python/schemas/store/refresh.py +109 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/search.py +1 -1
- snap_python-0.1.2/src/snap_python/scrape.py +130 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/utils.py +14 -0
- snap_python-0.1.0/PKG-INFO +0 -52
- snap_python-0.1.0/README.md +0 -32
- {snap_python-0.1.0 → snap_python-0.1.2}/LICENSE +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/__init__.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/__init__.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/config.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/changes.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/common.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/config.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/snaps.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/__init__.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/categories.py +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/category_by_name_schema.json +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/category_schema.json +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/snaps_find_schema.json +0 -0
- {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/snaps_info_schema.json +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: snap-python
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Interact with snapd and snapstore(s) from Python
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Alexander Lukens
|
|
7
|
+
Author-email: alexdlukens@gmail.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
16
|
+
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
17
|
+
Requires-Dist: retry (>=0.9.2,<0.10.0)
|
|
18
|
+
Project-URL: Documentation, https://alexdlukens.github.io/snap-python/
|
|
19
|
+
Project-URL: Repository, https://github.com/alexdlukens/snap-python
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# snap-python
|
|
23
|
+
|
|
24
|
+
snap-python provides an API client to interact with snapd
|
|
25
|
+
|
|
26
|
+
[Click here to read the documentation](https://alexdlukens.github.io/snap-python/)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
To install snap-python, you can use pip:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install snap-python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Here is a simple example of how to use snap-python:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# uses asyncio, must be run in an event loop
|
|
42
|
+
>>> import asyncio
|
|
43
|
+
>>> from snap_python.client import SnapClient
|
|
44
|
+
>>> sc = SnapClient()
|
|
45
|
+
>>> installed_snaps = asyncio.run(sc.snaps.list_installed_snaps())
|
|
46
|
+
>>> [snap.name for snap in installed_snaps.result]
|
|
47
|
+
[..., 'vlc', 'snapcraft', 'firefox', 'store-tui', 'gtk-common-themes', 'thunderbird', ...]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Get details for installed snap `vlc`
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
|
|
54
|
+
# uses asyncio, must be run in an event loop
|
|
55
|
+
>>> import asyncio
|
|
56
|
+
>>> from snap_python.client import SnapClient
|
|
57
|
+
>>> sc = SnapClient()
|
|
58
|
+
>>> vlc_snap = asyncio.run(sc.snaps.get_snap_info("vlc"))
|
|
59
|
+
>>> vlc_snap.result.version
|
|
60
|
+
'3.0.20-1-g2617de71b6'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
66
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# snap-python
|
|
2
|
+
|
|
3
|
+
snap-python provides an API client to interact with snapd
|
|
4
|
+
|
|
5
|
+
[Click here to read the documentation](https://alexdlukens.github.io/snap-python/)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
To install snap-python, you can use pip:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install snap-python
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Here is a simple example of how to use snap-python:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# uses asyncio, must be run in an event loop
|
|
21
|
+
>>> import asyncio
|
|
22
|
+
>>> from snap_python.client import SnapClient
|
|
23
|
+
>>> sc = SnapClient()
|
|
24
|
+
>>> installed_snaps = asyncio.run(sc.snaps.list_installed_snaps())
|
|
25
|
+
>>> [snap.name for snap in installed_snaps.result]
|
|
26
|
+
[..., 'vlc', 'snapcraft', 'firefox', 'store-tui', 'gtk-common-themes', 'thunderbird', ...]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Get details for installed snap `vlc`
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
|
|
33
|
+
# uses asyncio, must be run in an event loop
|
|
34
|
+
>>> import asyncio
|
|
35
|
+
>>> from snap_python.client import SnapClient
|
|
36
|
+
>>> sc = SnapClient()
|
|
37
|
+
>>> vlc_snap = asyncio.run(sc.snaps.get_snap_info("vlc"))
|
|
38
|
+
>>> vlc_snap.result.version
|
|
39
|
+
'3.0.20-1-g2617de71b6'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "snap-python"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Interact with snapd and snapstore(s) from Python"
|
|
5
5
|
authors = ["Alexander Lukens <alexdlukens@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
repository = "https://github.com/
|
|
8
|
+
repository = "https://github.com/alexdlukens/snap-python"
|
|
9
|
+
documentation = "https://alexdlukens.github.io/snap-python/"
|
|
9
10
|
|
|
10
11
|
[tool.poetry.dependencies]
|
|
11
12
|
python = "^3.10"
|
|
@@ -23,6 +24,7 @@ ipykernel = "^6.29.5"
|
|
|
23
24
|
pytest-cov = "^6.0.0"
|
|
24
25
|
sphinx = "^8.1.3"
|
|
25
26
|
sphinx-rtd-theme = "^3.0.2"
|
|
27
|
+
toml = "^0.10.2"
|
|
26
28
|
|
|
27
29
|
[build-system]
|
|
28
30
|
requires = ["poetry-core"]
|
|
@@ -65,16 +65,19 @@ class SnapClient(AbstractSnapsClient):
|
|
|
65
65
|
|
|
66
66
|
@retry(httpx.HTTPError, tries=3, delay=1, backoff=1)
|
|
67
67
|
async def request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
|
|
68
|
-
"""
|
|
68
|
+
r"""
|
|
69
69
|
Sends an HTTP request to the specified endpoint using the given method and parameters.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
|
|
71
|
+
:param method: The HTTP method to use for the request (e.g., 'GET', 'POST').
|
|
72
|
+
:type method: str
|
|
73
|
+
:param endpoint: The API endpoint to send the request to.
|
|
74
|
+
:type endpoint: str
|
|
75
|
+
:param `**kwargs`: Additional keyword arguments to pass to the request.
|
|
76
|
+
|
|
77
|
+
:returns: The response object from the HTTP request.
|
|
78
|
+
:rtype: httpx.Response
|
|
79
|
+
|
|
80
|
+
:raises httpx.HTTPStatusError: If the response contains an HTTP status code indicating an error.
|
|
78
81
|
"""
|
|
79
82
|
|
|
80
83
|
response = await self.snapd_client.request(
|
|
@@ -85,16 +88,19 @@ class SnapClient(AbstractSnapsClient):
|
|
|
85
88
|
|
|
86
89
|
@retry(httpx.HTTPError, tries=3, delay=1, backoff=1)
|
|
87
90
|
async def request_raw(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
|
|
88
|
-
"""
|
|
91
|
+
r"""
|
|
89
92
|
Sends an HTTP request using the specified method and endpoint.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
|
|
94
|
+
:param method: The HTTP method to use for the request (e.g., 'GET', 'POST').
|
|
95
|
+
:type method: str
|
|
96
|
+
:param endpoint: The endpoint URL to send the request to.
|
|
97
|
+
:type endpoint: str
|
|
98
|
+
:param `**kwargs`: Additional keyword arguments to pass to the request.
|
|
99
|
+
|
|
100
|
+
:returns: The response object resulting from the HTTP request.
|
|
101
|
+
:rtype: httpx.Response
|
|
102
|
+
|
|
103
|
+
:raises httpx.HTTPStatusError: If the response contains an HTTP status code indicating an error.
|
|
98
104
|
"""
|
|
99
105
|
|
|
100
106
|
response = await self.snapd_client.request(method, endpoint, **kwargs)
|
|
@@ -103,10 +109,11 @@ class SnapClient(AbstractSnapsClient):
|
|
|
103
109
|
return response
|
|
104
110
|
|
|
105
111
|
async def ping(self) -> httpx.Response:
|
|
106
|
-
"""
|
|
112
|
+
"""
|
|
113
|
+
Reserved for human-readable content describing the service.
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
:returns: The response from the ping request.
|
|
116
|
+
:rtype: httpx.Response
|
|
110
117
|
"""
|
|
111
118
|
response = await self.snapd_client.get(f"{self._base_url}/")
|
|
112
119
|
|
|
@@ -116,12 +123,14 @@ class SnapClient(AbstractSnapsClient):
|
|
|
116
123
|
async def get_changes_by_id(self, change_id: str) -> ChangesResponse:
|
|
117
124
|
"""
|
|
118
125
|
Asynchronously retrieves changes by their ID.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
|
|
127
|
+
:param change_id: The ID of the change to retrieve.
|
|
128
|
+
:type change_id: str
|
|
129
|
+
|
|
130
|
+
:returns: The response object containing the changes.
|
|
131
|
+
:rtype: ChangesResponse
|
|
132
|
+
|
|
133
|
+
:raises httpx.HTTPStatusError: If the request results in an HTTP error.
|
|
125
134
|
"""
|
|
126
135
|
|
|
127
136
|
try:
|
|
@@ -140,16 +149,19 @@ class SnapClient(AbstractSnapsClient):
|
|
|
140
149
|
) -> AsyncGenerator[ChangesResponse, None]:
|
|
141
150
|
"""
|
|
142
151
|
Asynchronous generator to fetch changes by change ID.
|
|
152
|
+
|
|
143
153
|
This generator continuously requests changes from the server using the provided
|
|
144
154
|
change ID and yields the response as a ChangesResponse object. It includes error
|
|
145
155
|
handling for HTTP status errors and adds a small delay between requests to avoid
|
|
146
156
|
overwhelming the server.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
|
|
158
|
+
:param change_id: The ID of the change to fetch.
|
|
159
|
+
:type change_id: str
|
|
160
|
+
|
|
161
|
+
:yields: The response object containing the changes.
|
|
162
|
+
:rtype: ChangesResponse
|
|
163
|
+
|
|
164
|
+
:raises httpx.HTTPStatusError: If the request fails with an HTTP status error.
|
|
153
165
|
"""
|
|
154
166
|
while True:
|
|
155
167
|
try:
|
|
@@ -21,6 +21,15 @@ class SnapsEndpoints:
|
|
|
21
21
|
self.common_endpoint = "snaps"
|
|
22
22
|
|
|
23
23
|
async def list_installed_snaps(self) -> InstalledSnapListResponse:
|
|
24
|
+
"""
|
|
25
|
+
Asynchronously retrieves a list of installed snaps.
|
|
26
|
+
|
|
27
|
+
:returns: The response containing the list of installed snaps.
|
|
28
|
+
:rtype: InstalledSnapListResponse
|
|
29
|
+
|
|
30
|
+
:raises httpx.HTTPStatusError: If the response status code does not indicate success.
|
|
31
|
+
"""
|
|
32
|
+
|
|
24
33
|
response: httpx.Response = await self._client.request(
|
|
25
34
|
"GET", self.common_endpoint
|
|
26
35
|
)
|
|
@@ -35,6 +44,17 @@ class SnapsEndpoints:
|
|
|
35
44
|
return response
|
|
36
45
|
|
|
37
46
|
async def get_snap_info(self, snap: str) -> SingleInstalledSnapResponse:
|
|
47
|
+
"""
|
|
48
|
+
Retrieves information about a specific snap.
|
|
49
|
+
|
|
50
|
+
:param snap: The name of the snap to retrieve information for.
|
|
51
|
+
:type snap: str
|
|
52
|
+
|
|
53
|
+
:returns: The response containing information about the snap.
|
|
54
|
+
:rtype: SingleInstalledSnapResponse
|
|
55
|
+
|
|
56
|
+
:raises httpx.HTTPStatusError: If the response status code does not indicate success.
|
|
57
|
+
"""
|
|
38
58
|
try:
|
|
39
59
|
response: httpx.Response = await self._client.request(
|
|
40
60
|
"GET", f"{self.common_endpoint}/{snap}"
|
|
@@ -50,6 +70,16 @@ class SnapsEndpoints:
|
|
|
50
70
|
return SingleInstalledSnapResponse.model_validate_json(response.content)
|
|
51
71
|
|
|
52
72
|
async def is_snap_installed(self, snap: str) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if a snap package is installed.
|
|
75
|
+
|
|
76
|
+
:param snap: The name of the snap package to check.
|
|
77
|
+
:type snap: str
|
|
78
|
+
|
|
79
|
+
:returns: True if the snap package is installed, False otherwise.
|
|
80
|
+
:rtype: bool
|
|
81
|
+
"""
|
|
82
|
+
|
|
53
83
|
snap_info = await self.get_snap_info(snap)
|
|
54
84
|
if snap_info.status == "OK":
|
|
55
85
|
return True
|
|
@@ -67,27 +97,36 @@ class SnapsEndpoints:
|
|
|
67
97
|
filename: str = None,
|
|
68
98
|
wait: bool = False,
|
|
69
99
|
) -> AsyncResponse | ChangesResponse:
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
To sideload a snap, provide the filename parameter with the path to the snap file
|
|
100
|
+
"""
|
|
101
|
+
Install or sideload a snap.
|
|
73
102
|
|
|
103
|
+
To sideload a snap, provide the filename parameter with the path to the snap file.
|
|
74
104
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
:param snap: Name of the snap to install.
|
|
106
|
+
:type snap: str
|
|
107
|
+
:param channel: Channel to install, defaults to "stable".
|
|
108
|
+
:type channel: str, optional
|
|
109
|
+
:param classic: Install with classic confinement, defaults to False.
|
|
110
|
+
:type classic: bool, optional
|
|
111
|
+
:param dangerous: 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.
|
|
112
|
+
:type dangerous: bool, optional
|
|
113
|
+
:param devmode: Install with devmode, defaults to False.
|
|
114
|
+
:type devmode: bool, optional
|
|
115
|
+
:param jailmode: Install snap with jailmode, defaults to False.
|
|
116
|
+
:type jailmode: bool, optional
|
|
117
|
+
:param revision: Install a specific revision of the snap, defaults to None.
|
|
118
|
+
:type revision: int, optional
|
|
119
|
+
:param filename: Path to snap to sideload, defaults to None.
|
|
120
|
+
:type filename: str, optional
|
|
121
|
+
:param wait: Whether to wait for snap to install. If not waiting, will return async response with change id, defaults to False.
|
|
122
|
+
:type wait: bool, optional
|
|
85
123
|
|
|
86
|
-
|
|
87
|
-
|
|
124
|
+
:raises FileNotFoundError: If the specified snap file does not exist.
|
|
125
|
+
:raises ValueError: If attempting to sideload without the dangerous flag set to True.
|
|
126
|
+
:raises SnapdAPIError: If there is an error during the snap install.
|
|
88
127
|
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
:returns: If wait is True, will return ChangesResponse. Otherwise, will return AsyncResponse.
|
|
129
|
+
:rtype: AsyncResponse | ChangesResponse
|
|
91
130
|
"""
|
|
92
131
|
request_data = {
|
|
93
132
|
"action": "install",
|
|
@@ -149,15 +188,20 @@ class SnapsEndpoints:
|
|
|
149
188
|
) -> AsyncResponse | ChangesResponse:
|
|
150
189
|
"""
|
|
151
190
|
Asynchronously removes a snap package.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
191
|
+
|
|
192
|
+
:param snap: The name of the snap package to remove.
|
|
193
|
+
:type snap: str
|
|
194
|
+
:param purge: If True, purges the snap package. Defaults to False.
|
|
195
|
+
:type purge: bool, optional
|
|
196
|
+
:param terminate: If True, terminates the snap package. Defaults to False.
|
|
197
|
+
:type terminate: bool, optional
|
|
198
|
+
:param wait: If True, waits for the removal process to complete. Defaults to False.
|
|
199
|
+
:type wait: bool, optional
|
|
200
|
+
|
|
201
|
+
:returns: The response from the snapd API, either an asynchronous response or a changes response if waiting for completion.
|
|
202
|
+
:rtype: AsyncResponse | ChangesResponse
|
|
203
|
+
|
|
204
|
+
:raises SnapdAPIError: If there is an error in the snap removal process.
|
|
161
205
|
"""
|
|
162
206
|
request_data = {
|
|
163
207
|
"action": "remove",
|
|
@@ -206,23 +250,34 @@ class SnapsEndpoints:
|
|
|
206
250
|
) -> AsyncResponse | ChangesResponse:
|
|
207
251
|
"""
|
|
208
252
|
Refreshes a snap package.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
253
|
+
|
|
254
|
+
:param snap: The name of the snap package to refresh.
|
|
255
|
+
:type snap: str
|
|
256
|
+
:param channel: The channel to refresh the snap from. Defaults to "stable".
|
|
257
|
+
:type channel: str, optional
|
|
258
|
+
:param classic: Whether to use classic confinement. Defaults to False.
|
|
259
|
+
:type classic: bool, optional
|
|
260
|
+
:param dangerous: Whether to allow installation of unasserted snaps. Defaults to False.
|
|
261
|
+
:type dangerous: bool, optional
|
|
262
|
+
:param devmode: Whether to use development mode. Defaults to False.
|
|
263
|
+
:type devmode: bool, optional
|
|
264
|
+
:param ignore_validation: Whether to ignore validation. Defaults to False.
|
|
265
|
+
:type ignore_validation: bool, optional
|
|
266
|
+
:param jailmode: Whether to use jail mode. Defaults to False.
|
|
267
|
+
:type jailmode: bool, optional
|
|
268
|
+
:param revision: The specific revision to refresh to. Defaults to None.
|
|
269
|
+
:type revision: int, optional
|
|
270
|
+
:param filename: The path to the snap file for sideloading. Defaults to None.
|
|
271
|
+
:type filename: str, optional
|
|
272
|
+
:param wait: Whether to wait for the refresh operation to complete. Defaults to False.
|
|
273
|
+
:type wait: bool, optional
|
|
274
|
+
|
|
275
|
+
:returns: The response from the refresh operation.
|
|
276
|
+
:rtype: AsyncResponse | ChangesResponse
|
|
277
|
+
|
|
278
|
+
:raises FileNotFoundError: If the specified snap file does not exist.
|
|
279
|
+
:raises ValueError: If attempting to sideload without the dangerous flag set to True.
|
|
280
|
+
:raises SnapdAPIError: If there is an error during the snap refresh.
|
|
226
281
|
"""
|
|
227
282
|
request_data = {
|
|
228
283
|
"action": "refresh",
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import uuid
|
|
2
3
|
|
|
3
4
|
import retry
|
|
4
|
-
from httpx import AsyncClient
|
|
5
|
+
from httpx import AsyncClient, Response
|
|
5
6
|
|
|
6
7
|
from snap_python.schemas.store.categories import (
|
|
7
8
|
VALID_CATEGORY_FIELDS,
|
|
8
9
|
CategoryResponse,
|
|
9
10
|
SingleCategoryResponse,
|
|
10
11
|
)
|
|
11
|
-
from snap_python.schemas.store.info import
|
|
12
|
+
from snap_python.schemas.store.info import (
|
|
13
|
+
VALID_SNAP_INFO_FIELDS,
|
|
14
|
+
InfoResponse,
|
|
15
|
+
)
|
|
16
|
+
from snap_python.schemas.store.refresh import (
|
|
17
|
+
VALID_SNAP_REFRESH_FIELDS,
|
|
18
|
+
RefreshRevisionResponse,
|
|
19
|
+
)
|
|
12
20
|
from snap_python.schemas.store.search import (
|
|
13
21
|
VALID_SEARCH_CATEGORY_FIELDS,
|
|
14
22
|
ArchSearchResponse,
|
|
@@ -37,6 +45,19 @@ class StoreEndpoints:
|
|
|
37
45
|
self.store_client.headers.update(headers)
|
|
38
46
|
|
|
39
47
|
async def get_snap_details(self, snap_name: str, fields: list[str] | None = None):
|
|
48
|
+
"""
|
|
49
|
+
Get details of a snap.
|
|
50
|
+
|
|
51
|
+
:param snap_name: The name of the snap.
|
|
52
|
+
:type snap_name: str
|
|
53
|
+
:param fields: The fields to include in the response.
|
|
54
|
+
:type fields: list[str], optional
|
|
55
|
+
|
|
56
|
+
:returns: The snap details.
|
|
57
|
+
:rtype: dict
|
|
58
|
+
|
|
59
|
+
:raises ValueError: If invalid fields are provided.
|
|
60
|
+
"""
|
|
40
61
|
query = {}
|
|
41
62
|
if fields is not None:
|
|
42
63
|
if not all(field in VALID_SEARCH_CATEGORY_FIELDS for field in fields):
|
|
@@ -51,7 +72,22 @@ class StoreEndpoints:
|
|
|
51
72
|
response.raise_for_status()
|
|
52
73
|
return response.json()
|
|
53
74
|
|
|
54
|
-
async def get_snap_info(
|
|
75
|
+
async def get_snap_info(
|
|
76
|
+
self, snap_name: str, fields: list[str] | None = None
|
|
77
|
+
) -> InfoResponse:
|
|
78
|
+
"""
|
|
79
|
+
Get information about a snap.
|
|
80
|
+
|
|
81
|
+
:param snap_name: The name of the snap.
|
|
82
|
+
:type snap_name: str
|
|
83
|
+
:param fields: The fields to include in the response.
|
|
84
|
+
:type fields: list[str], optional
|
|
85
|
+
|
|
86
|
+
:returns: The snap information.
|
|
87
|
+
:rtype: InfoResponse
|
|
88
|
+
|
|
89
|
+
:raises ValueError: If invalid fields are provided.
|
|
90
|
+
"""
|
|
55
91
|
query = {}
|
|
56
92
|
if fields is not None:
|
|
57
93
|
if not all(field in VALID_SNAP_INFO_FIELDS for field in fields):
|
|
@@ -69,6 +105,19 @@ class StoreEndpoints:
|
|
|
69
105
|
async def get_categories(
|
|
70
106
|
self, type: str | None = None, fields: list[str] | None = None
|
|
71
107
|
) -> CategoryResponse:
|
|
108
|
+
"""
|
|
109
|
+
Get categories of snaps.
|
|
110
|
+
|
|
111
|
+
:param type: The type of categories.
|
|
112
|
+
:type type: str, optional
|
|
113
|
+
:param fields: The fields to include in the response.
|
|
114
|
+
:type fields: list[str], optional
|
|
115
|
+
|
|
116
|
+
:returns: The categories.
|
|
117
|
+
:rtype: CategoryResponse
|
|
118
|
+
|
|
119
|
+
:raises ValueError: If invalid fields are provided.
|
|
120
|
+
"""
|
|
72
121
|
query = {}
|
|
73
122
|
if fields is not None:
|
|
74
123
|
if not all(field in VALID_CATEGORY_FIELDS for field in fields):
|
|
@@ -86,6 +135,19 @@ class StoreEndpoints:
|
|
|
86
135
|
async def get_category_by_name(
|
|
87
136
|
self, name: str, fields: list[str] | None = None
|
|
88
137
|
) -> SingleCategoryResponse:
|
|
138
|
+
"""
|
|
139
|
+
Get a category by name.
|
|
140
|
+
|
|
141
|
+
:param name: The name of the category.
|
|
142
|
+
:type name: str
|
|
143
|
+
:param fields: The fields to include in the response.
|
|
144
|
+
:type fields: list[str], optional
|
|
145
|
+
|
|
146
|
+
:returns: The category.
|
|
147
|
+
:rtype: SingleCategoryResponse
|
|
148
|
+
|
|
149
|
+
:raises ValueError: If invalid fields are provided.
|
|
150
|
+
"""
|
|
89
151
|
query = {}
|
|
90
152
|
if fields is not None:
|
|
91
153
|
if not all(field in VALID_CATEGORY_FIELDS for field in fields):
|
|
@@ -114,10 +176,39 @@ class StoreEndpoints:
|
|
|
114
176
|
publisher: str | None = None,
|
|
115
177
|
headers: dict[str, str] | None = None,
|
|
116
178
|
) -> SearchResponse:
|
|
117
|
-
"""from https://api.snapcraft.io/docs/search.html#snaps_find
|
|
118
|
-
and https://snapcraft.io/docs/snapd-api#heading--find
|
|
119
179
|
"""
|
|
180
|
+
Search for snaps in the store.
|
|
181
|
+
|
|
182
|
+
:param query: The search query.
|
|
183
|
+
:type query: str, optional
|
|
184
|
+
:param fields: The fields to include in the response.
|
|
185
|
+
:type fields: str, optional
|
|
186
|
+
:param name_startswith: Filter snaps by name prefix.
|
|
187
|
+
:type name_startswith: str, optional
|
|
188
|
+
:param architecture: Filter snaps by architecture.
|
|
189
|
+
:type architecture: str, optional
|
|
190
|
+
:param common_id: Filter snaps by common ID.
|
|
191
|
+
:type common_id: str, optional
|
|
192
|
+
:param category: Filter snaps by category.
|
|
193
|
+
:type category: str, optional
|
|
194
|
+
:param channel: Filter snaps by channel.
|
|
195
|
+
:type channel: str, optional
|
|
196
|
+
:param confiement: Filter snaps by confinement.
|
|
197
|
+
:type confiement: str, optional
|
|
198
|
+
:param featured: Filter snaps by featured status.
|
|
199
|
+
:type featured: bool, optional
|
|
200
|
+
:param private: Filter snaps by private status.
|
|
201
|
+
:type private: bool, optional
|
|
202
|
+
:param publisher: Filter snaps by publisher.
|
|
203
|
+
:type publisher: str, optional
|
|
204
|
+
:param headers: Additional headers to include in the request.
|
|
205
|
+
:type headers: dict[str, str], optional
|
|
120
206
|
|
|
207
|
+
:returns: The search response.
|
|
208
|
+
:rtype: SearchResponse
|
|
209
|
+
|
|
210
|
+
:raises ValueError: If invalid fields are provided.
|
|
211
|
+
"""
|
|
121
212
|
route = "/snaps/find"
|
|
122
213
|
query_dict: dict = {
|
|
123
214
|
"q": query,
|
|
@@ -160,14 +251,45 @@ class StoreEndpoints:
|
|
|
160
251
|
|
|
161
252
|
@retry.retry(Exception, tries=3, delay=2, backoff=2)
|
|
162
253
|
async def retry_get_snap_info(self, snap_name: str, fields: list[str]):
|
|
254
|
+
"""
|
|
255
|
+
Retry getting snap information.
|
|
256
|
+
|
|
257
|
+
:param snap_name: The name of the snap.
|
|
258
|
+
:type snap_name: str
|
|
259
|
+
:param fields: The fields to include in the response.
|
|
260
|
+
:type fields: list[str]
|
|
261
|
+
|
|
262
|
+
:returns: The snap information.
|
|
263
|
+
:rtype: InfoResponse
|
|
264
|
+
"""
|
|
163
265
|
return await self.get_snap_info(snap_name=snap_name, fields=fields)
|
|
164
266
|
|
|
165
267
|
async def get_top_snaps_from_category(self, category: str) -> SearchResponse:
|
|
268
|
+
"""
|
|
269
|
+
Get top snaps from a category.
|
|
270
|
+
|
|
271
|
+
:param category: The category name.
|
|
272
|
+
:type category: str
|
|
273
|
+
|
|
274
|
+
:returns: The search response.
|
|
275
|
+
:rtype: SearchResponse
|
|
276
|
+
"""
|
|
166
277
|
return await self.find(
|
|
167
278
|
category=category, fields=["title", "store-url", "summary"]
|
|
168
279
|
)
|
|
169
280
|
|
|
170
281
|
async def get_all_snaps_for_arch(self, arch: str) -> ArchSearchResponse:
|
|
282
|
+
"""
|
|
283
|
+
Get all snaps for a given architecture.
|
|
284
|
+
|
|
285
|
+
:param arch: The architecture.
|
|
286
|
+
:type arch: str
|
|
287
|
+
|
|
288
|
+
:returns: The search response.
|
|
289
|
+
:rtype: ArchSearchResponse
|
|
290
|
+
|
|
291
|
+
:raises ValueError: If invalid architecture is provided.
|
|
292
|
+
"""
|
|
171
293
|
# use the old "/api/v1/snaps/names" to get all snaps for a given architecture
|
|
172
294
|
|
|
173
295
|
# ensure valid arch
|
|
@@ -193,3 +315,84 @@ class StoreEndpoints:
|
|
|
193
315
|
response_json = response.json()
|
|
194
316
|
response_json["arch"] = arch
|
|
195
317
|
return ArchSearchResponse.model_validate(response_json)
|
|
318
|
+
|
|
319
|
+
async def snap_refresh(
|
|
320
|
+
self, snap_name: str, payload: dict, extra_headers: dict = None
|
|
321
|
+
) -> Response:
|
|
322
|
+
"""Query the Store's "snap_refresh" endpoint.
|
|
323
|
+
|
|
324
|
+
Useful for more than refreshing a snap. Notably some snap information is only retrievable from this endpoint. E.g. old revision information
|
|
325
|
+
|
|
326
|
+
:param snap_name: The name of the snap.
|
|
327
|
+
:type snap_name: str
|
|
328
|
+
"""
|
|
329
|
+
route = "/snaps/refresh"
|
|
330
|
+
if extra_headers is None:
|
|
331
|
+
extra_headers = {}
|
|
332
|
+
query = {}
|
|
333
|
+
response = await self.store_client.post(
|
|
334
|
+
f"{self.base_url}{route}", json=payload, headers=extra_headers, params=query
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if not response.is_success:
|
|
338
|
+
# TODO: Add logging here
|
|
339
|
+
pass
|
|
340
|
+
return response
|
|
341
|
+
|
|
342
|
+
async def get_snap_revision_info(
|
|
343
|
+
self, snap_name: str, revision: int, arch: str, fields=None
|
|
344
|
+
) -> RefreshRevisionResponse:
|
|
345
|
+
"""Get information about a snap revision.
|
|
346
|
+
|
|
347
|
+
:param snap_name: The name of the snap.
|
|
348
|
+
:type snap_name: str
|
|
349
|
+
:param revision: The revision of the snap.
|
|
350
|
+
:type revision: int
|
|
351
|
+
:param arch: The architecture of the snap to retrieve details about (e.g. amd64, arm64, riscv64, etc).
|
|
352
|
+
:type arch: str
|
|
353
|
+
|
|
354
|
+
:returns: The snap revision information.
|
|
355
|
+
:rtype: RefreshRevisionResponse
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
# cast revision to int
|
|
359
|
+
revision = int(revision)
|
|
360
|
+
extra_headers = {"Snap-Device-Architecture": arch}
|
|
361
|
+
snap_info = await self.get_snap_info(snap_name=snap_name)
|
|
362
|
+
|
|
363
|
+
# I don't think this matters for the "context" field, so using info from the first available
|
|
364
|
+
channel_map_item = snap_info.channel_map[0]
|
|
365
|
+
|
|
366
|
+
payload = {
|
|
367
|
+
"context": [
|
|
368
|
+
{
|
|
369
|
+
"tracking-channel": "stable",
|
|
370
|
+
"snap-id": snap_info.snap_id,
|
|
371
|
+
"revision": channel_map_item.revision,
|
|
372
|
+
"instance-key": str(uuid.uuid4()),
|
|
373
|
+
}
|
|
374
|
+
],
|
|
375
|
+
"actions": [
|
|
376
|
+
{
|
|
377
|
+
"action": "download",
|
|
378
|
+
"name": snap_name,
|
|
379
|
+
"revision": revision,
|
|
380
|
+
"instance-key": str(uuid.uuid4()),
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if fields is not None:
|
|
386
|
+
if not all(field in VALID_SNAP_REFRESH_FIELDS for field in fields):
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"Invalid fields. Allowed fields: {VALID_SNAP_REFRESH_FIELDS}"
|
|
389
|
+
)
|
|
390
|
+
payload["fields"] = fields
|
|
391
|
+
|
|
392
|
+
response = await self.snap_refresh(
|
|
393
|
+
snap_name=snap_name,
|
|
394
|
+
payload=payload,
|
|
395
|
+
extra_headers=extra_headers,
|
|
396
|
+
)
|
|
397
|
+
response.raise_for_status()
|
|
398
|
+
return RefreshRevisionResponse.model_validate_json(response.content)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
from typing import List, Optional
|
|
6
6
|
|
|
7
|
-
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
7
|
+
from pydantic import AliasChoices, AwareDatetime, BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
9
|
from snap_python.schemas.snaps import StoreSnap
|
|
10
10
|
|
|
@@ -47,7 +47,11 @@ class Channel(BaseModel):
|
|
|
47
47
|
|
|
48
48
|
architecture: str
|
|
49
49
|
name: str
|
|
50
|
-
released_at: Optional[AwareDatetime] = Field(
|
|
50
|
+
released_at: Optional[AwareDatetime] = Field(
|
|
51
|
+
None,
|
|
52
|
+
alias=AliasChoices("released-at", "released_at"),
|
|
53
|
+
serialization_alias="released-at",
|
|
54
|
+
)
|
|
51
55
|
risk: str
|
|
52
56
|
track: str
|
|
53
57
|
|
|
@@ -56,7 +60,9 @@ class Delta(BaseModel):
|
|
|
56
60
|
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
57
61
|
|
|
58
62
|
format: str
|
|
59
|
-
sha3_384: str = Field(
|
|
63
|
+
sha3_384: Optional[str] = Field(
|
|
64
|
+
None, alias=AliasChoices("sha3-384", "sha3_384"), serialization_alias="sha3-384"
|
|
65
|
+
)
|
|
60
66
|
size: float
|
|
61
67
|
source: float
|
|
62
68
|
target: float
|
|
@@ -67,7 +73,9 @@ class Download(BaseModel):
|
|
|
67
73
|
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
68
74
|
|
|
69
75
|
deltas: List[Delta]
|
|
70
|
-
sha3_384: str = Field(
|
|
76
|
+
sha3_384: Optional[str] = Field(
|
|
77
|
+
None, alias=AliasChoices("sha3-384", "sha3_384"), serialization_alias="sha3-384"
|
|
78
|
+
)
|
|
71
79
|
size: float
|
|
72
80
|
url: str
|
|
73
81
|
|
|
@@ -80,7 +88,9 @@ class Epoch(BaseModel):
|
|
|
80
88
|
|
|
81
89
|
|
|
82
90
|
class Download1(BaseModel):
|
|
83
|
-
sha3_384: Optional[str] = Field(
|
|
91
|
+
sha3_384: Optional[str] = Field(
|
|
92
|
+
None, alias=AliasChoices("sha3-384", "sha3_384"), serialization_alias="sha3-384"
|
|
93
|
+
)
|
|
84
94
|
size: Optional[int] = None
|
|
85
95
|
url: Optional[str] = None
|
|
86
96
|
|
|
@@ -89,7 +99,11 @@ class Resource(BaseModel):
|
|
|
89
99
|
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
90
100
|
|
|
91
101
|
architectures: Optional[List[str]] = None
|
|
92
|
-
created_at: Optional[str] = Field(
|
|
102
|
+
created_at: Optional[str] = Field(
|
|
103
|
+
None,
|
|
104
|
+
alias=AliasChoices("created-at", "created_at"),
|
|
105
|
+
serialization_alias="created-at",
|
|
106
|
+
)
|
|
93
107
|
description: Optional[str] = None
|
|
94
108
|
download: Optional[Download1] = None
|
|
95
109
|
name: Optional[str] = None
|
|
@@ -104,14 +118,26 @@ class ChannelMapItem(BaseModel):
|
|
|
104
118
|
architectures: Optional[List[str]] = None
|
|
105
119
|
base: Optional[str] = None
|
|
106
120
|
channel: Channel
|
|
107
|
-
common_ids: Optional[List[str]] = Field(
|
|
121
|
+
common_ids: Optional[List[str]] = Field(
|
|
122
|
+
None,
|
|
123
|
+
alias=AliasChoices("common-ids", "common_ids"),
|
|
124
|
+
serialization_alias="common-ids",
|
|
125
|
+
)
|
|
108
126
|
confinement: Optional[str] = None
|
|
109
|
-
created_at: Optional[AwareDatetime] = Field(
|
|
127
|
+
created_at: Optional[AwareDatetime] = Field(
|
|
128
|
+
None,
|
|
129
|
+
alias=AliasChoices("created-at", "created_at"),
|
|
130
|
+
serialization_alias="created-at",
|
|
131
|
+
)
|
|
110
132
|
download: Optional[Download] = None
|
|
111
133
|
epoch: Optional[Epoch] = None
|
|
112
134
|
resources: Optional[List[Resource]] = None
|
|
113
135
|
revision: Optional[int] = None
|
|
114
|
-
snap_yaml: Optional[str] = Field(
|
|
136
|
+
snap_yaml: Optional[str] = Field(
|
|
137
|
+
None,
|
|
138
|
+
alias=AliasChoices("snap-yaml", "snap_yaml"),
|
|
139
|
+
serialization_alias="snap-yaml",
|
|
140
|
+
)
|
|
115
141
|
type: Optional[str] = None
|
|
116
142
|
version: Optional[str] = None
|
|
117
143
|
|
|
@@ -126,9 +152,23 @@ class ErrorListItem(BaseModel):
|
|
|
126
152
|
class InfoResponse(BaseModel):
|
|
127
153
|
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
128
154
|
|
|
129
|
-
channel_map: List[ChannelMapItem] = Field(
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
channel_map: List[ChannelMapItem] = Field(
|
|
156
|
+
...,
|
|
157
|
+
alias=AliasChoices("channel-map", "channel_map"),
|
|
158
|
+
serialization_alias="channel-map",
|
|
159
|
+
)
|
|
160
|
+
default_track: Optional[str] = Field(
|
|
161
|
+
None,
|
|
162
|
+
alias=AliasChoices("default-track", "default_track"),
|
|
163
|
+
serialization_alias="default-track",
|
|
164
|
+
)
|
|
165
|
+
error_list: Optional[List[ErrorListItem]] = Field(
|
|
166
|
+
None,
|
|
167
|
+
alias=AliasChoices("error-list", "error_list"),
|
|
168
|
+
serialization_alias="error-list",
|
|
169
|
+
)
|
|
132
170
|
name: str
|
|
133
171
|
snap: StoreSnap
|
|
134
|
-
snap_id: str = Field(
|
|
172
|
+
snap_id: str = Field(
|
|
173
|
+
..., alias=AliasChoices("snap-id", "snap_id"), serialization_alias="snap-id"
|
|
174
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import AliasChoices, AwareDatetime, BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from snap_python.schemas.snaps import Snap
|
|
6
|
+
from snap_python.schemas.store.info import Download, ErrorListItem, Resource
|
|
7
|
+
|
|
8
|
+
VALID_SNAP_REFRESH_FIELDS = [
|
|
9
|
+
"architectures",
|
|
10
|
+
"base",
|
|
11
|
+
"common-ids",
|
|
12
|
+
"confinement",
|
|
13
|
+
"contact",
|
|
14
|
+
"created-at",
|
|
15
|
+
"description",
|
|
16
|
+
"download",
|
|
17
|
+
"epoch",
|
|
18
|
+
"gated-snap-ids",
|
|
19
|
+
"license",
|
|
20
|
+
"links",
|
|
21
|
+
"media",
|
|
22
|
+
"name",
|
|
23
|
+
"prices",
|
|
24
|
+
"private",
|
|
25
|
+
"publisher",
|
|
26
|
+
"resources",
|
|
27
|
+
"revision",
|
|
28
|
+
"snap-id",
|
|
29
|
+
"snap-yaml",
|
|
30
|
+
"summary",
|
|
31
|
+
"title",
|
|
32
|
+
"type",
|
|
33
|
+
"version",
|
|
34
|
+
"website",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SnapRefreshFields(BaseModel):
|
|
39
|
+
architectures: Optional[list[str]] = None
|
|
40
|
+
base: Optional[str] = None
|
|
41
|
+
common_ids: Optional[list[str]] = Field(
|
|
42
|
+
None, alias=AliasChoices("common-ids", "common_ids")
|
|
43
|
+
)
|
|
44
|
+
confinement: Optional[str] = None
|
|
45
|
+
created_at: Optional[AwareDatetime] = Field(
|
|
46
|
+
None,
|
|
47
|
+
alias=AliasChoices("created-at", "created_at"),
|
|
48
|
+
serialization_alias="created-at",
|
|
49
|
+
)
|
|
50
|
+
download: Optional[Download] = None
|
|
51
|
+
epoch: Optional[dict[str, list[int]]] = None
|
|
52
|
+
gated_snap_ids: Optional[list[str]] = Field(
|
|
53
|
+
None, alias=AliasChoices("gated-snap-ids", "gated_snap_ids")
|
|
54
|
+
)
|
|
55
|
+
license: Optional[str] = None
|
|
56
|
+
prices: Optional[dict[str, str]] = None
|
|
57
|
+
resources: list[Resource] = Field(default_factory=list)
|
|
58
|
+
revision: Optional[int] = None
|
|
59
|
+
snap_id: Optional[str] = Field(
|
|
60
|
+
None, alias=AliasChoices("snap-id", "snap_id"), serialization_alias="snap-id"
|
|
61
|
+
)
|
|
62
|
+
snap_yaml: Optional[str] = Field(
|
|
63
|
+
None,
|
|
64
|
+
alias=AliasChoices("snap-yaml", "snap_yaml"),
|
|
65
|
+
serialization_alias="snap-yaml",
|
|
66
|
+
)
|
|
67
|
+
type: Optional[str] = None
|
|
68
|
+
version: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StoreRefreshSnap(Snap, SnapRefreshFields):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RefreshResultData(BaseModel):
|
|
76
|
+
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
77
|
+
|
|
78
|
+
default_track: Optional[str] = Field(
|
|
79
|
+
None,
|
|
80
|
+
alias=AliasChoices("default-track", "default_track"),
|
|
81
|
+
serialization_alias="default-track",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
snap: StoreRefreshSnap
|
|
86
|
+
instance_key: str = Field(
|
|
87
|
+
...,
|
|
88
|
+
alias=AliasChoices("instance-key", "instance_key"),
|
|
89
|
+
serialization_alias="instance-key",
|
|
90
|
+
)
|
|
91
|
+
released_at: Optional[AwareDatetime] = Field(
|
|
92
|
+
None,
|
|
93
|
+
alias=AliasChoices("released-at", "released_at"),
|
|
94
|
+
serialization_alias="released-at",
|
|
95
|
+
)
|
|
96
|
+
result: str
|
|
97
|
+
|
|
98
|
+
snap_id: str = Field(
|
|
99
|
+
..., alias=AliasChoices("snap-id", "snap_id"), serialization_alias="snap-id"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RefreshRevisionResponse(BaseModel):
|
|
104
|
+
error_list: Optional[List[ErrorListItem]] = Field(
|
|
105
|
+
None,
|
|
106
|
+
alias=AliasChoices("error-list", "error_list"),
|
|
107
|
+
serialization_alias="error-list",
|
|
108
|
+
)
|
|
109
|
+
results: List[RefreshResultData]
|
|
@@ -142,6 +142,6 @@ class ArchSearchItem(BaseModel):
|
|
|
142
142
|
|
|
143
143
|
class ArchSearchResponse(BaseModel):
|
|
144
144
|
results: list[ArchSearchItem] = Field(
|
|
145
|
-
alias=AliasPath("_embedded", "clickindex:package")
|
|
145
|
+
alias=AliasChoices(AliasPath("_embedded", "clickindex:package"), "results")
|
|
146
146
|
)
|
|
147
147
|
arch: str
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from snap_python.client import SnapClient
|
|
4
|
+
from snap_python.schemas.store.info import ChannelMapItem
|
|
5
|
+
from snap_python.schemas.store.refresh import VALID_SNAP_REFRESH_FIELDS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_highest_revision(channel_map: list[ChannelMapItem]) -> ChannelMapItem:
|
|
9
|
+
"""Find the ChannelMapItem with the highest revision number from a list of ChannelMapItems.
|
|
10
|
+
|
|
11
|
+
Find the highest revision in a provided channel map
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
channel_map (list[ChannelMapItem]): A list of ChannelMapItem objects to search through.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
ChannelMapItem: The item with the highest revision number
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
|
|
21
|
+
>>> items = [ChannelMapItem(revision=1), ChannelMapItem(revision=5), ChannelMapItem(revision=3)]
|
|
22
|
+
>>> highest = get_highest_revision(items)
|
|
23
|
+
>>> highest.revision
|
|
24
|
+
5
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
print(channel_map)
|
|
29
|
+
return max(channel_map, key=lambda x: x.revision)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def download_snap_file(
|
|
33
|
+
snap_client: SnapClient,
|
|
34
|
+
snap_revision_download: str,
|
|
35
|
+
snap_revision_download_path: pathlib.Path,
|
|
36
|
+
):
|
|
37
|
+
"""Asynchronously downloads a snap file from the Snap Store.
|
|
38
|
+
|
|
39
|
+
This function streams the snap file from the specified URL and writes it to
|
|
40
|
+
the specified path on disk.
|
|
41
|
+
|
|
42
|
+
Parameters:
|
|
43
|
+
snap_client (SnapClient): The client for interacting with the Snap Store.
|
|
44
|
+
snap_revision_download (str): The URL to download the snap file from.
|
|
45
|
+
snap_revision_download_path (pathlib.Path): The path where the downloaded snap file will be saved.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
None
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
store_client = snap_client.store.store_client
|
|
53
|
+
async with store_client.stream(
|
|
54
|
+
"GET", snap_revision_download, follow_redirects=True
|
|
55
|
+
) as response:
|
|
56
|
+
with open(snap_revision_download_path, "wb") as f:
|
|
57
|
+
async for chunk in response.aiter_bytes():
|
|
58
|
+
f.write(chunk)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def get_all_snap_content(
|
|
62
|
+
snap_client: SnapClient,
|
|
63
|
+
snap_name: str,
|
|
64
|
+
output_dir: pathlib.Path | str,
|
|
65
|
+
start_revision: int = 1,
|
|
66
|
+
with_snap_files: bool = False,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Asynchronously fetches and downloads all content for a specified snap package across multiple revisions.
|
|
70
|
+
|
|
71
|
+
This function retrieves metadata for each revision of a snap package and optionally downloads
|
|
72
|
+
the snap files themselves. All content is organized into directories by revision number.
|
|
73
|
+
|
|
74
|
+
Parameters:
|
|
75
|
+
snap_client (SnapClient): Initialized Snap client instance for API interactions.
|
|
76
|
+
snap_name (str): Name of the snap package to process.
|
|
77
|
+
output_dir (pathlib.Path | str): Directory where snap content will be saved.
|
|
78
|
+
start_revision (int, optional): First revision to process. Defaults to 1.
|
|
79
|
+
with_snap_files (bool, optional): Whether to download the actual snap package files. Defaults to False.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
None
|
|
83
|
+
|
|
84
|
+
Notes:
|
|
85
|
+
- Creates a directory structure where each revision has its own subdirectory
|
|
86
|
+
- Saves metadata as data.json in each revision directory
|
|
87
|
+
- When with_snap_files=True, also downloads the snap package file for each revision
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
if not isinstance(output_dir, pathlib.Path):
|
|
91
|
+
output_dir = pathlib.Path(output_dir)
|
|
92
|
+
|
|
93
|
+
if not output_dir.exists():
|
|
94
|
+
output_dir.mkdir(parents=True)
|
|
95
|
+
|
|
96
|
+
current_snap_info = await snap_client.store.get_snap_info(
|
|
97
|
+
snap_name, fields=["name", "channel-map", "revision"]
|
|
98
|
+
)
|
|
99
|
+
current_snap_channel = get_highest_revision(current_snap_info.channel_map)
|
|
100
|
+
|
|
101
|
+
print(
|
|
102
|
+
f"Processing snap {snap_name} from revision {start_revision} to {current_snap_channel.revision}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
for revision in range(1, current_snap_channel.revision + 1):
|
|
106
|
+
print(f"Processing revision {revision}")
|
|
107
|
+
revision_dir = output_dir / str(revision)
|
|
108
|
+
revision_dir.mkdir(exist_ok=True)
|
|
109
|
+
# get snap revision info
|
|
110
|
+
snap_revision_info = await snap_client.store.get_snap_revision_info(
|
|
111
|
+
snap_name, revision, arch="amd64", fields=VALID_SNAP_REFRESH_FIELDS
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
snap_revision_data = snap_revision_info.results[0]
|
|
115
|
+
|
|
116
|
+
with open(revision_dir / "data.json", "w") as f:
|
|
117
|
+
f.write(snap_revision_data.model_dump_json(indent=2))
|
|
118
|
+
|
|
119
|
+
# download snap revision
|
|
120
|
+
snap_revision_download = snap_revision_data.snap.download.url
|
|
121
|
+
|
|
122
|
+
# download the file
|
|
123
|
+
snap_revision_download_path = (
|
|
124
|
+
revision_dir / snap_revision_download.split("/")[-1]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if with_snap_files:
|
|
128
|
+
await download_snap_file(
|
|
129
|
+
snap_client, snap_revision_download, snap_revision_download_path
|
|
130
|
+
)
|
|
@@ -6,6 +6,10 @@ from snap_python.schemas.changes import ChangesResponse
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class SnapdAPIError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
General Exception raised for errors in the Snapd API.
|
|
11
|
+
"""
|
|
12
|
+
|
|
9
13
|
pass
|
|
10
14
|
|
|
11
15
|
|
|
@@ -24,6 +28,16 @@ class AbstractSnapsClient(ABC): # pragma: no cover
|
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
def going_to_reload_daemon(changes: ChangesResponse | None) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Determines if a daemon reload is required based on the provided changes.
|
|
33
|
+
|
|
34
|
+
:param changes: An instance of ChangesResponse or None.
|
|
35
|
+
:type changes: ChangesResponse | None
|
|
36
|
+
|
|
37
|
+
:returns: True if a daemon reload or restart is required, False otherwise.
|
|
38
|
+
:rtype: bool
|
|
39
|
+
"""
|
|
40
|
+
|
|
27
41
|
if changes is None or not isinstance(changes, ChangesResponse):
|
|
28
42
|
return False
|
|
29
43
|
|
snap_python-0.1.0/PKG-INFO
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: snap-python
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: An api to interact with snapd and snap stores
|
|
5
|
-
License: MIT
|
|
6
|
-
Author: Alexander Lukens
|
|
7
|
-
Author-email: alexdlukens@gmail.com
|
|
8
|
-
Requires-Python: >=3.10,<4.0
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
16
|
-
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
17
|
-
Requires-Dist: retry (>=0.9.2,<0.10.0)
|
|
18
|
-
Project-URL: Repository, https://github.com/alexlukens/snap-python
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
|
|
21
|
-
# snap_python
|
|
22
|
-
|
|
23
|
-
snap_python provides an API client to interact with snapd
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
26
|
-
|
|
27
|
-
To install snap_python, you can use pip:
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
pip install snap_python
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Usage
|
|
34
|
-
|
|
35
|
-
Here is a simple example of how to use snap_python:
|
|
36
|
-
|
|
37
|
-
```python
|
|
38
|
-
# TODO
|
|
39
|
-
```
|
|
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
|
-
|
|
50
|
-
## License
|
|
51
|
-
|
|
52
|
-
This project is licensed under the MIT License. See the LICENSE file for more details.
|
snap_python-0.1.0/README.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# snap_python
|
|
2
|
-
|
|
3
|
-
snap_python provides an API client to interact with snapd
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
To install snap_python, you can use pip:
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
pip install snap_python
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Usage
|
|
14
|
-
|
|
15
|
-
Here is a simple example of how to use snap_python:
|
|
16
|
-
|
|
17
|
-
```python
|
|
18
|
-
# TODO
|
|
19
|
-
```
|
|
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
|
-
|
|
30
|
-
## License
|
|
31
|
-
|
|
32
|
-
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
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.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/category_schema.json
RENAMED
|
File without changes
|
{snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/snaps_find_schema.json
RENAMED
|
File without changes
|
{snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/snaps_info_schema.json
RENAMED
|
File without changes
|