snap-python 0.0.5__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
snap_python/client.py CHANGED
@@ -44,15 +44,15 @@ class SnapClient(AbstractSnapsClient):
44
44
  if store_headers is None:
45
45
  store_headers = {"Snap-Device-Series": "16", "X-Ubuntu-Series": "16"}
46
46
 
47
- snapd_headers = {}
47
+ self.snapd_headers = {}
48
48
  if prompt_for_authentication:
49
- snapd_headers = {"X-Allow-Interaction": "true"}
49
+ self.snapd_headers = {"X-Allow-Interaction": "true"}
50
50
 
51
51
  self.version = version
52
52
  self.store_base_url = store_base_url
53
53
  self.store_headers = store_headers
54
54
  self.snapd_client = httpx.AsyncClient(
55
- transport=self._transport, headers=snapd_headers
55
+ transport=self._transport, headers=self.snapd_headers
56
56
  )
57
57
  self.snaps = SnapsEndpoints(self)
58
58
  self.store = StoreEndpoints(
@@ -61,6 +61,7 @@ class SnapClient(AbstractSnapsClient):
61
61
  headers=self.store_headers,
62
62
  )
63
63
  self.config = ConfigEndpoints(self)
64
+ self.snapd_client = httpx.AsyncClient(transport=self._transport)
64
65
 
65
66
  @retry(httpx.HTTPError, tries=3, delay=1, backoff=1)
66
67
  async def request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
@@ -111,30 +112,54 @@ class SnapClient(AbstractSnapsClient):
111
112
 
112
113
  return response
113
114
 
115
+ @retry(httpx.HTTPError, tries=3, delay=0.5)
114
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
+
115
127
  try:
116
128
  response = await self.request("GET", f"changes/{change_id}")
117
129
  except httpx.HTTPStatusError as e:
118
130
  logger.warning(
119
- f"Bad response status code for get_changes_by_id: {e.response.status_code}"
131
+ "Bad response status code for get_changes_by_id: %s",
132
+ e.response.status_code,
120
133
  )
121
134
  response = e.response
122
- pass
123
135
 
124
136
  return ChangesResponse.model_validate_json(response.content)
125
137
 
126
138
  async def get_changes_by_id_generator(
127
139
  self, change_id: str
128
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
+ """
129
154
  while True:
130
155
  try:
131
156
  response = await self.request("GET", f"changes/{change_id}")
132
157
  except httpx.HTTPStatusError as e:
133
158
  logger.warning(
134
- f"Bad response status code for get_changes_by_id: {e.response.status_code}"
159
+ "Bad response status code for get_changes_by_id: %s",
160
+ e.response.status_code,
135
161
  )
136
162
  response = e.response
137
- pass
138
163
 
139
164
  change_response = ChangesResponse.model_validate_json(response.content)
140
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
 
@@ -62,7 +62,6 @@ class SnapsEndpoints:
62
62
  classic: bool = False,
63
63
  dangerous: bool = False,
64
64
  devmode: bool = False,
65
- ignore_validation: bool = False,
66
65
  jailmode: bool = False,
67
66
  revision: int = None,
68
67
  filename: str = None,
@@ -79,14 +78,13 @@ class SnapsEndpoints:
79
78
  classic (bool, optional): Install with classic confinement. Defaults to False.
80
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.
81
80
  devmode (bool, optional): Install with devmode. Defaults to False.
82
- ignore_validation (bool, optional): _description_. Defaults to False.
83
81
  jailmode (bool, optional): Install snap with jailmode. Defaults to False.
84
82
  revision (int, optional): install a specific revision of the snap. Defaults to None.
85
83
  filename (str, optional): Path to snap to sideload. Defaults to None.
86
84
  wait (bool, optional): Whether to wait for snap to install. If not waiting, will return async response with change id. Defaults to False.
87
85
 
88
86
  Raises:
89
- Exception: If error occurs during snap install
87
+ SnapdAPIError: If error occurs during snap install
90
88
 
91
89
  Returns:
92
90
  AsyncResponse | ChangesResponse: If wait is True, will return ChangesResponse. Otherwise, will return AsyncResponse
@@ -97,7 +95,6 @@ class SnapsEndpoints:
97
95
  "classic": classic,
98
96
  "dangerous": dangerous,
99
97
  "devmode": devmode,
100
- "ignore_validation": ignore_validation,
101
98
  "jailmode": jailmode,
102
99
  }
103
100
  if revision:
@@ -128,17 +125,16 @@ class SnapsEndpoints:
128
125
  while True:
129
126
  try:
130
127
  changes = await self._client.get_changes_by_id(changes_id)
131
- logger.debug(f"Progress: {changes.result.overall_progress}")
132
- except httpx.HTTPError as e:
128
+ logger.debug("Progress: %s", changes.result.overall_progress)
129
+ except httpx.HTTPError:
133
130
  if going_to_reload_daemon(previous_changes):
134
131
  logger.debug("Waiting for daemon to reload")
135
- await asyncio.sleep(0.1)
136
- continue
132
+ changes = previous_changes
137
133
 
138
134
  if changes.ready:
139
135
  break
140
136
  if changes.result.err:
141
- raise Exception(f"Error in snap install: {changes.result.err}")
137
+ raise SnapdAPIError(f"Error in snap install: {changes.result.err}")
142
138
  await asyncio.sleep(0.1)
143
139
  previous_changes = changes
144
140
  return changes
@@ -151,6 +147,18 @@ class SnapsEndpoints:
151
147
  terminate: bool = False,
152
148
  wait: bool = False,
153
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
+ """
154
162
  request_data = {
155
163
  "action": "remove",
156
164
  "purge": purge,
@@ -168,7 +176,7 @@ class SnapsEndpoints:
168
176
  while True:
169
177
  try:
170
178
  changes = await self._client.get_changes_by_id(changes_id)
171
- except httpx.HTTPError as e:
179
+ except httpx.HTTPError:
172
180
  if going_to_reload_daemon(previous_changes):
173
181
  logger.debug("Waiting for daemon to reload")
174
182
  await asyncio.sleep(0.1)
@@ -176,9 +184,95 @@ class SnapsEndpoints:
176
184
  if changes.ready:
177
185
  break
178
186
  if changes.result.err:
179
- raise Exception(f"Error in snap remove: {changes.result.err}")
187
+ raise SnapdAPIError(f"Error in snap remove: {changes.result.err}")
180
188
  await asyncio.sleep(0.1)
181
189
  previous_changes = changes
182
190
  return changes
183
191
 
184
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)
@@ -1,6 +1,13 @@
1
1
  from typing import Any, Dict, List, Optional
2
2
 
3
- from pydantic import AliasChoices, AwareDatetime, BaseModel, ConfigDict, Field
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
13
  from snap_python.schemas.snaps import InstalledSnap, Snap, StoreSnap, StoreSnapFields
@@ -120,3 +127,21 @@ class SearchResponse(BaseModel):
120
127
 
121
128
  error_list: Optional[List[ErrorListItem]] = Field(None, alias="error-list")
122
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
snap_python/utils.py CHANGED
@@ -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
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: snap-python
3
- Version: 0.0.5
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
@@ -1,9 +1,9 @@
1
1
  snap_python/__init__.py,sha256=gV43Nempk7HFKD1UCvxUhmP9x6dBmPjotcITREr81Vg,222
2
- snap_python/client.py,sha256=skmAjn_-aOrnV5aRw74vRwLbYDi-dqaV4mNSa69_JA4,5369
2
+ snap_python/client.py,sha256=iiKryiJSqyHj_zPHyQHB0RgydwVql6IwgJ2Nny9HPNY,6492
3
3
  snap_python/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  snap_python/components/config.py,sha256=IO_gCrumphEZioINFmXxb8I5OVsKC9cgSa28sQbZ700,1775
5
- snap_python/components/snaps.py,sha256=hY054pk7CwMvannJcuHG_MxYCUsvUs-gXAWvsB8o40M,7150
6
- snap_python/components/store.py,sha256=_o_2OYMvGbq_bLEQXw9zHjBrCipQ92qZaSkwdAfngAc,6451
5
+ snap_python/components/snaps.py,sha256=etqnlFcfaBPiOeIb-r13kSY-KnPdvv4WE1LDv15u-YU,11551
6
+ snap_python/components/store.py,sha256=MWs2gh-ZdsuLiGsN9LJDHsPf-a0B206QzzLEbzKbwh0,7326
7
7
  snap_python/schemas/changes.py,sha256=7SYUFXAQq9arl7hAA3YQ0YubMurPJtknTOVxP8ryqew,1919
8
8
  snap_python/schemas/common.py,sha256=GLtC9aBo3O8vLKX9PNvzE-_DKN02OGpwsS7Ik2WK9-8,2608
9
9
  snap_python/schemas/config.py,sha256=xXTGhk6TpwI8RlR6_qnfWlBHrBnTwr3hRCO5qCnN-6U,107
@@ -15,9 +15,9 @@ snap_python/schemas/store/json/category_by_name_schema.json,sha256=3-95m6V2WXaPC
15
15
  snap_python/schemas/store/json/category_schema.json,sha256=c4h_eyyBbChOiNby3hYfjAh9i_1MDbPJnHHU4rGOGss,1898
16
16
  snap_python/schemas/store/json/snaps_find_schema.json,sha256=TfKpnievs5QOc7ah97ATMCQTOEfKspONTZnwSwq_0fI,8866
17
17
  snap_python/schemas/store/json/snaps_info_schema.json,sha256=udoOovb6Ea7EnE_R9RuZ1CpY1wOJ3-K8xP4-8zKHgrQ,15888
18
- snap_python/schemas/store/search.py,sha256=z0SutS4LQJybrNY9WqgU5CWOXJi-VeL2K3EKxveFpQc,3300
19
- snap_python/utils.py,sha256=pE-mVtoq5KMZH_Xg1O9lJlAmktLyuIdAVBEomuLZjWI,752
20
- snap_python-0.0.5.dist-info/LICENSE,sha256=lYXNZszxC_l2rlBvrHYs-esDegxWh8MKtjrdxA24uOk,1072
21
- snap_python-0.0.5.dist-info/METADATA,sha256=g7i8TJybXyJDFEJHDj5Ix5f_9yelR9mpFa75pjhUutQ,1324
22
- snap_python-0.0.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
- snap_python-0.0.5.dist-info/RECORD,,
18
+ snap_python/schemas/store/search.py,sha256=u92aFLLuvDwpfMNHlZo7O2-k-kj7cgzAOtvxxsvi6mU,3718
19
+ snap_python/utils.py,sha256=RzNnbcA-ca-lU-3-2uW-2dCYkMt4O0uxme-XMgv9Wrs,871
20
+ snap_python-0.1.0.dist-info/LICENSE,sha256=lYXNZszxC_l2rlBvrHYs-esDegxWh8MKtjrdxA24uOk,1072
21
+ snap_python-0.1.0.dist-info/METADATA,sha256=oghLA_J2xPWkVR7En0-ctujzxAEP1FvGaDZj6sE2dfI,1271
22
+ snap_python-0.1.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
+ snap_python-0.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.0.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any