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.
Files changed (27) hide show
  1. snap_python-0.1.2/PKG-INFO +66 -0
  2. snap_python-0.1.2/README.md +44 -0
  3. {snap_python-0.1.0 → snap_python-0.1.2}/pyproject.toml +5 -3
  4. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/client.py +45 -33
  5. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/snaps.py +98 -43
  6. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/store.py +208 -5
  7. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/info.py +53 -13
  8. snap_python-0.1.2/src/snap_python/schemas/store/refresh.py +109 -0
  9. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/search.py +1 -1
  10. snap_python-0.1.2/src/snap_python/scrape.py +130 -0
  11. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/utils.py +14 -0
  12. snap_python-0.1.0/PKG-INFO +0 -52
  13. snap_python-0.1.0/README.md +0 -32
  14. {snap_python-0.1.0 → snap_python-0.1.2}/LICENSE +0 -0
  15. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/__init__.py +0 -0
  16. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/__init__.py +0 -0
  17. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/components/config.py +0 -0
  18. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/changes.py +0 -0
  19. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/common.py +0 -0
  20. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/config.py +0 -0
  21. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/snaps.py +0 -0
  22. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/__init__.py +0 -0
  23. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/categories.py +0 -0
  24. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/category_by_name_schema.json +0 -0
  25. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/category_schema.json +0 -0
  26. {snap_python-0.1.0 → snap_python-0.1.2}/src/snap_python/schemas/store/json/snaps_find_schema.json +0 -0
  27. {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.0"
4
- description = "An api to interact with snapd and snap stores"
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/alexlukens/snap-python"
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
- Args:
71
- method (str): The HTTP method to use for the request (e.g., 'GET', 'POST').
72
- endpoint (str): The API endpoint to send the request to.
73
- **kwargs: Additional keyword arguments to pass to the request.
74
- Returns:
75
- httpx.Response: The response object from the HTTP request.
76
- Raises:
77
- httpx.HTTPStatusError: If the response contains an HTTP status code indicating an error.
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
- Args:
91
- method (str): The HTTP method to use for the request (e.g., 'GET', 'POST').
92
- endpoint (str): The endpoint URL to send the request to.
93
- **kwargs: Additional keyword arguments to pass to the request.
94
- Returns:
95
- httpx.Response: The response object resulting from the HTTP request.
96
- Raises:
97
- httpx.HTTPStatusError: If the response contains an HTTP status code indicating an error.
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
- """Reserved for human-readable content describing the service.
112
+ """
113
+ Reserved for human-readable content describing the service.
107
114
 
108
- Returns:
109
- httpx.Response: _description_
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
- 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.
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
- 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.
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
- """Install or sideload a snap
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
- Args:
76
- snap (str): name of the snap to install
77
- channel (str, optional): Channel to install. Defaults to "stable".
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.
80
- devmode (bool, optional): Install with devmode. Defaults to False.
81
- jailmode (bool, optional): Install snap with jailmode. Defaults to False.
82
- revision (int, optional): install a specific revision of the snap. Defaults to None.
83
- filename (str, optional): Path to snap to sideload. Defaults to None.
84
- wait (bool, optional): Whether to wait for snap to install. If not waiting, will return async response with change id. Defaults to False.
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
- Raises:
87
- SnapdAPIError: If error occurs during snap install
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
- Returns:
90
- AsyncResponse | ChangesResponse: If wait is True, will return ChangesResponse. Otherwise, will return AsyncResponse
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
- 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.
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
- 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.
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 VALID_SNAP_INFO_FIELDS, InfoResponse
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(self, snap_name: str, fields: list[str] | None = None):
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(None, alias="released-at")
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(..., alias="sha3-384")
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(..., alias="sha3-384")
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(None, alias="sha3-384")
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(None, alias="created-at")
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(None, alias="common-ids")
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(None, alias="created-at")
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(None, alias="snap-yaml")
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(..., alias="channel-map")
130
- default_track: Optional[str] = Field(None, alias="default-track")
131
- error_list: Optional[List[ErrorListItem]] = Field(None, alias="error-list")
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(..., alias="snap-id")
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
 
@@ -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.
@@ -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