snap-python 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snap_python-0.0.1/LICENSE +21 -0
- snap_python-0.0.1/PKG-INFO +39 -0
- snap_python-0.0.1/README.md +23 -0
- snap_python-0.0.1/pyproject.toml +26 -0
- snap_python-0.0.1/src/snap_python/__init__.py +0 -0
- snap_python-0.0.1/src/snap_python/client.py +86 -0
- snap_python-0.0.1/src/snap_python/components/__init__.py +0 -0
- snap_python-0.0.1/src/snap_python/components/snaps.py +92 -0
- snap_python-0.0.1/src/snap_python/components/store.py +167 -0
- snap_python-0.0.1/src/snap_python/schemas/changes.py +51 -0
- snap_python-0.0.1/src/snap_python/schemas/common.py +64 -0
- snap_python-0.0.1/src/snap_python/schemas/snaps.py +64 -0
- snap_python-0.0.1/src/snap_python/schemas/store/__init__.py +0 -0
- snap_python-0.0.1/src/snap_python/schemas/store/categories.py +42 -0
- snap_python-0.0.1/src/snap_python/schemas/store/info.py +134 -0
- snap_python-0.0.1/src/snap_python/schemas/store/json/category_by_name_schema.json +61 -0
- snap_python-0.0.1/src/snap_python/schemas/store/json/category_schema.json +61 -0
- snap_python-0.0.1/src/snap_python/schemas/store/json/snaps_find_schema.json +221 -0
- snap_python-0.0.1/src/snap_python/schemas/store/json/snaps_info_schema.json +432 -0
- snap_python-0.0.1/src/snap_python/schemas/store/search.py +160 -0
- snap_python-0.0.1/src/snap_python/utils.py +19 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alexander Lukens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: snap-python
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary:
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Alexander Lukens
|
|
7
|
+
Author-email: alexdlukens@gmail.com
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
13
|
+
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
14
|
+
Requires-Dist: retry (>=0.9.2,<0.10.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# snap_python
|
|
18
|
+
|
|
19
|
+
snap_python provides an API client to interact with snapd
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
To install snap_python, you can use pip:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pip install snap_python
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Here is a simple example of how to use snap_python:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# TODO
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
## License
|
|
22
|
+
|
|
23
|
+
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "snap-python"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Alexander Lukens <alexdlukens@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.12"
|
|
11
|
+
httpx = "^0.27.2"
|
|
12
|
+
pydantic = "^2.9.2"
|
|
13
|
+
retry = "^0.9.2"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.group.dev.dependencies]
|
|
16
|
+
pylxd = "^2.3.5"
|
|
17
|
+
pytest = "^8.3.3"
|
|
18
|
+
pytest-asyncio = "^0.24.0"
|
|
19
|
+
ruff = "^0.7.4"
|
|
20
|
+
pre-commit = "^4.0.1"
|
|
21
|
+
ipykernel = "^6.29.5"
|
|
22
|
+
pytest-cov = "^6.0.0"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from snap_python.components.snaps import SnapsEndpoints
|
|
6
|
+
from snap_python.components.store import StoreEndpoints
|
|
7
|
+
from snap_python.schemas.changes import ChangesResponse
|
|
8
|
+
from snap_python.utils import AbstractSnapsClient
|
|
9
|
+
|
|
10
|
+
SNAPD_SOCKET = "/run/snapd.socket"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("snap_python.client")
|
|
13
|
+
logger.setLevel(logging.DEBUG)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SnapClient(AbstractSnapsClient):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
version: str = "v2",
|
|
20
|
+
snapd_socket_location: str = None,
|
|
21
|
+
tcp_location: str = None,
|
|
22
|
+
store_base_url: str = "https://api.snapcraft.io",
|
|
23
|
+
store_headers: dict[str, str] = None,
|
|
24
|
+
):
|
|
25
|
+
if tcp_location and snapd_socket_location:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"Only one of snapd_socket_location or tcp_location can be provided."
|
|
28
|
+
)
|
|
29
|
+
if tcp_location is not None:
|
|
30
|
+
self._base_url = tcp_location
|
|
31
|
+
self._transport = httpx.AsyncHTTPTransport()
|
|
32
|
+
else:
|
|
33
|
+
self._base_url = "http://localhost"
|
|
34
|
+
self._transport = httpx.AsyncHTTPTransport(
|
|
35
|
+
uds=snapd_socket_location or SNAPD_SOCKET
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if store_headers is None:
|
|
39
|
+
store_headers = {"Snap-Device-Series": "16", "X-Ubuntu-Series": "16"}
|
|
40
|
+
|
|
41
|
+
self.version = version
|
|
42
|
+
self.store_base_url = store_base_url
|
|
43
|
+
self.store_headers = store_headers
|
|
44
|
+
self.snapd_client = httpx.AsyncClient(transport=self._transport)
|
|
45
|
+
self.snaps = SnapsEndpoints(self)
|
|
46
|
+
self.store = StoreEndpoints(
|
|
47
|
+
base_url=self.store_base_url,
|
|
48
|
+
version=self.version,
|
|
49
|
+
headers=self.store_headers,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
|
|
53
|
+
response = await self.snapd_client.request(
|
|
54
|
+
method, f"{self._base_url}/{self.version}/{endpoint}", **kwargs
|
|
55
|
+
)
|
|
56
|
+
logger.debug(f"response: {response.content}")
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
return response
|
|
59
|
+
|
|
60
|
+
async def request_raw(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
|
|
61
|
+
response = await self.snapd_client.request(method, endpoint, **kwargs)
|
|
62
|
+
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
async def ping(self) -> httpx.Response:
|
|
67
|
+
"""Reserved for human-readable content describing the service.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
httpx.Response: _description_
|
|
71
|
+
"""
|
|
72
|
+
response = await self.snapd_client.get(f"{self._base_url}/")
|
|
73
|
+
|
|
74
|
+
return response
|
|
75
|
+
|
|
76
|
+
async def get_changes_by_id(self, change_id: str) -> ChangesResponse:
|
|
77
|
+
try:
|
|
78
|
+
response = await self.request("GET", f"changes/{change_id}")
|
|
79
|
+
except httpx.HTTPStatusError as e:
|
|
80
|
+
logger.warning(
|
|
81
|
+
f"Bad response status code for get_changes_by_id: {e.response.status_code}"
|
|
82
|
+
)
|
|
83
|
+
response = e.response
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
return ChangesResponse.model_validate_json(response.content)
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from snap_python.schemas.changes import ChangesResponse
|
|
6
|
+
from snap_python.schemas.common import AsyncResponse
|
|
7
|
+
from snap_python.schemas.snaps import SnapListResponse
|
|
8
|
+
from snap_python.utils import AbstractSnapsClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SnapsEndpoints:
|
|
12
|
+
def __init__(self, client: AbstractSnapsClient) -> None:
|
|
13
|
+
self._client = client
|
|
14
|
+
self.common_endpoint = "snaps"
|
|
15
|
+
|
|
16
|
+
async def list_installed_snaps(self) -> SnapListResponse:
|
|
17
|
+
response: httpx.Response = await self._client.request(
|
|
18
|
+
"GET", self.common_endpoint
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
response = SnapListResponse.model_validate_json(response.content)
|
|
22
|
+
if response.status_code > 299:
|
|
23
|
+
raise httpx.HTTPStatusError(
|
|
24
|
+
request=response.request,
|
|
25
|
+
response=response,
|
|
26
|
+
message=f"Invalid status code in response: {response.status_code}",
|
|
27
|
+
)
|
|
28
|
+
return response
|
|
29
|
+
|
|
30
|
+
async def install_snap(
|
|
31
|
+
self,
|
|
32
|
+
snap: str,
|
|
33
|
+
channel: str = "stable",
|
|
34
|
+
classic: bool = False,
|
|
35
|
+
devmode: bool = False,
|
|
36
|
+
ignore_validation: bool = False,
|
|
37
|
+
jailmode: bool = False,
|
|
38
|
+
revision: int = None,
|
|
39
|
+
wait: bool = False,
|
|
40
|
+
) -> AsyncResponse | ChangesResponse:
|
|
41
|
+
request_data = {
|
|
42
|
+
"action": "install",
|
|
43
|
+
"channel": channel,
|
|
44
|
+
"classic": classic,
|
|
45
|
+
"devmode": devmode,
|
|
46
|
+
"ignore_validation": ignore_validation,
|
|
47
|
+
"jailmode": jailmode,
|
|
48
|
+
}
|
|
49
|
+
if revision:
|
|
50
|
+
request_data["revision"] = revision
|
|
51
|
+
raw_response: httpx.Response = await self._client.request(
|
|
52
|
+
"POST", f"{self.common_endpoint}/{snap}", json=request_data
|
|
53
|
+
)
|
|
54
|
+
response = AsyncResponse.model_validate_json(raw_response.content)
|
|
55
|
+
if wait:
|
|
56
|
+
changes_id = response.change
|
|
57
|
+
while True:
|
|
58
|
+
changes = await self._client.get_changes_by_id(changes_id)
|
|
59
|
+
if changes.ready:
|
|
60
|
+
break
|
|
61
|
+
if changes.result.err:
|
|
62
|
+
raise Exception(f"Error in snap install: {changes.result.err}")
|
|
63
|
+
await asyncio.sleep(2.0)
|
|
64
|
+
return changes
|
|
65
|
+
return response
|
|
66
|
+
|
|
67
|
+
async def remove_snap(
|
|
68
|
+
self, snap: str, purge: bool, terminate: bool, wait: bool = False
|
|
69
|
+
) -> AsyncResponse | ChangesResponse:
|
|
70
|
+
request_data = {
|
|
71
|
+
"action": "remove",
|
|
72
|
+
"purge": purge,
|
|
73
|
+
"terminate": terminate,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
raw_response: httpx.Response = await self._client.request(
|
|
77
|
+
"POST", f"{self.common_endpoint}/{snap}", json=request_data
|
|
78
|
+
)
|
|
79
|
+
response = AsyncResponse.model_validate_json(raw_response.content)
|
|
80
|
+
|
|
81
|
+
if wait:
|
|
82
|
+
changes_id = response.change
|
|
83
|
+
while True:
|
|
84
|
+
changes = await self._client.get_changes_by_id(changes_id)
|
|
85
|
+
if changes.ready:
|
|
86
|
+
break
|
|
87
|
+
if changes.result.err:
|
|
88
|
+
raise Exception(f"Error in snap remove: {changes.result.err}")
|
|
89
|
+
await asyncio.sleep(2.0)
|
|
90
|
+
return changes
|
|
91
|
+
|
|
92
|
+
return response
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
import retry
|
|
4
|
+
from httpx import AsyncClient
|
|
5
|
+
|
|
6
|
+
from snap_python.schemas.store.categories import (
|
|
7
|
+
VALID_CATEGORY_FIELDS,
|
|
8
|
+
CategoryResponse,
|
|
9
|
+
SingleCategoryResponse,
|
|
10
|
+
)
|
|
11
|
+
from snap_python.schemas.store.info import VALID_SNAP_INFO_FIELDS, InfoResponse
|
|
12
|
+
from snap_python.schemas.store.search import (
|
|
13
|
+
VALID_SEARCH_CATEGORY_FIELDS,
|
|
14
|
+
SearchResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StoreEndpoints:
|
|
19
|
+
"""Query snap store that is available at <base_url> for information about snaps.
|
|
20
|
+
Calls made directly with store to enable querying the non-default snap store for information.
|
|
21
|
+
Snapd only supports the store at snapcraft.io
|
|
22
|
+
|
|
23
|
+
Certain functionality is not available in snapd, such as querying for/by categories
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, base_url: str, version: str, headers: dict[str, str] = None
|
|
28
|
+
) -> None:
|
|
29
|
+
self.store_client = AsyncClient()
|
|
30
|
+
self.store_client.request = functools.partial(
|
|
31
|
+
self.store_client.request, timeout=5
|
|
32
|
+
)
|
|
33
|
+
self.base_url = f"{base_url}/{version}"
|
|
34
|
+
self._raw_base_url = base_url
|
|
35
|
+
if headers is not None:
|
|
36
|
+
self.store_client.headers.update(headers)
|
|
37
|
+
|
|
38
|
+
async def get_snap_details(self, snap_name: str, fields: list[str] | None = None):
|
|
39
|
+
query = {}
|
|
40
|
+
if fields is not None:
|
|
41
|
+
if not all(field in VALID_SEARCH_CATEGORY_FIELDS for field in fields):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Invalid fields. Allowed fields: {VALID_SEARCH_CATEGORY_FIELDS}"
|
|
44
|
+
)
|
|
45
|
+
query["fields"] = ",".join(fields)
|
|
46
|
+
route = f"/api/v1/snaps/details/{snap_name}"
|
|
47
|
+
response = await self.store_client.get(
|
|
48
|
+
f"{self._raw_base_url}{route}", params=query
|
|
49
|
+
)
|
|
50
|
+
response.raise_for_status()
|
|
51
|
+
return response.json()
|
|
52
|
+
|
|
53
|
+
async def get_snap_info(self, snap_name: str, fields: list[str] | None = None):
|
|
54
|
+
query = {}
|
|
55
|
+
if fields is not None:
|
|
56
|
+
if not all(field in VALID_SNAP_INFO_FIELDS for field in fields):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Invalid fields. Allowed fields: {VALID_SNAP_INFO_FIELDS}"
|
|
59
|
+
)
|
|
60
|
+
query["fields"] = ",".join(fields)
|
|
61
|
+
route = f"/v2/snaps/info/{snap_name}"
|
|
62
|
+
response = await self.store_client.get(
|
|
63
|
+
f"{self._raw_base_url}{route}", params=query
|
|
64
|
+
)
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
return InfoResponse.model_validate_json(response.content)
|
|
67
|
+
|
|
68
|
+
async def get_categories(
|
|
69
|
+
self, type: str | None = None, fields: list[str] | None = None
|
|
70
|
+
) -> CategoryResponse:
|
|
71
|
+
query = {}
|
|
72
|
+
if fields is not None:
|
|
73
|
+
if not all(field in VALID_CATEGORY_FIELDS for field in fields):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Invalid fields. Allowed fields: {VALID_CATEGORY_FIELDS}"
|
|
76
|
+
)
|
|
77
|
+
query["fields"] = ",".join(fields)
|
|
78
|
+
if type is not None:
|
|
79
|
+
query["type"] = type
|
|
80
|
+
route = "/snaps/categories"
|
|
81
|
+
response = await self.store_client.get(f"{self.base_url}{route}", params=query)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
return CategoryResponse.model_validate_json(response.content)
|
|
84
|
+
|
|
85
|
+
async def get_category_by_name(
|
|
86
|
+
self, name: str, fields: list[str] | None = None
|
|
87
|
+
) -> SingleCategoryResponse:
|
|
88
|
+
query = {}
|
|
89
|
+
if fields is not None:
|
|
90
|
+
if not all(field in VALID_CATEGORY_FIELDS for field in fields):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Invalid fields. Allowed fields: {VALID_CATEGORY_FIELDS}"
|
|
93
|
+
)
|
|
94
|
+
query["fields"] = ",".join(fields)
|
|
95
|
+
|
|
96
|
+
route = f"/snaps/category/{name}"
|
|
97
|
+
response = await self.store_client.get(f"{self.base_url}{route}", params=query)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
return SingleCategoryResponse.model_validate_json(response.content)
|
|
100
|
+
|
|
101
|
+
async def find(
|
|
102
|
+
self,
|
|
103
|
+
query: str | None = None,
|
|
104
|
+
fields: str | None = None,
|
|
105
|
+
name_startswith: str | None = None,
|
|
106
|
+
architecture: str | None = None,
|
|
107
|
+
common_id: str | None = None,
|
|
108
|
+
category: str | None = None,
|
|
109
|
+
channel: str | None = None,
|
|
110
|
+
confiement: str | None = None,
|
|
111
|
+
featured: bool = False,
|
|
112
|
+
private: bool = False,
|
|
113
|
+
publisher: str | None = None,
|
|
114
|
+
headers: dict[str, str] | None = None,
|
|
115
|
+
) -> SearchResponse:
|
|
116
|
+
"""from https://api.snapcraft.io/docs/search.html#snaps_find
|
|
117
|
+
and https://snapcraft.io/docs/snapd-api#heading--find
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
route = "/snaps/find"
|
|
121
|
+
query_dict: dict = {
|
|
122
|
+
"q": query,
|
|
123
|
+
"name_startswith": name_startswith,
|
|
124
|
+
"architecture": architecture,
|
|
125
|
+
"common_id": common_id,
|
|
126
|
+
"category": category,
|
|
127
|
+
"channel": channel,
|
|
128
|
+
"confinement": confiement,
|
|
129
|
+
"featured": featured,
|
|
130
|
+
"private": private,
|
|
131
|
+
"publisher": publisher,
|
|
132
|
+
}
|
|
133
|
+
if fields is not None:
|
|
134
|
+
if not all(field in VALID_SEARCH_CATEGORY_FIELDS for field in fields):
|
|
135
|
+
bad_fields = [
|
|
136
|
+
field
|
|
137
|
+
for field in fields
|
|
138
|
+
if field not in VALID_SEARCH_CATEGORY_FIELDS
|
|
139
|
+
]
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Invalid fields: ({bad_fields}). Allowed fields: {VALID_SEARCH_CATEGORY_FIELDS}"
|
|
142
|
+
)
|
|
143
|
+
query_dict["fields"] = ",".join(fields)
|
|
144
|
+
extra_headers = headers or {}
|
|
145
|
+
query_dict = {
|
|
146
|
+
k: v for k, v in query_dict.items() if (v is not None) and (v != "")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# snap store expects "true" or "false" for boolean values
|
|
150
|
+
for key in ["featured", "private"]:
|
|
151
|
+
if key in query_dict:
|
|
152
|
+
query_dict[key] = str(query_dict[key]).lower()
|
|
153
|
+
|
|
154
|
+
response = await self.store_client.get(
|
|
155
|
+
f"{self.base_url}{route}", params=query_dict, headers=extra_headers
|
|
156
|
+
)
|
|
157
|
+
response.raise_for_status()
|
|
158
|
+
return SearchResponse.model_validate_json(response.content)
|
|
159
|
+
|
|
160
|
+
@retry.retry(Exception, tries=3, delay=2, backoff=2)
|
|
161
|
+
async def retry_get_snap_info(self, snap_name: str, fields: list[str]):
|
|
162
|
+
return await self.get_snap_info(snap_name=snap_name, fields=fields)
|
|
163
|
+
|
|
164
|
+
async def get_top_snaps_from_category(self, category: str) -> SearchResponse:
|
|
165
|
+
return await self.find(
|
|
166
|
+
category=category, fields=["title", "store-url", "summary"]
|
|
167
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from pydantic import AliasChoices, AwareDatetime, BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from snap_python.schemas.common import BaseErrorResult, BaseResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Task(BaseModel):
|
|
7
|
+
id: str
|
|
8
|
+
kind: str
|
|
9
|
+
summary: str
|
|
10
|
+
status: str
|
|
11
|
+
progress: dict
|
|
12
|
+
spawn_time: AwareDatetime = Field(
|
|
13
|
+
validation_alias=AliasChoices("spawn-time", "spawn_time"),
|
|
14
|
+
serialization_alias="spawn-time",
|
|
15
|
+
)
|
|
16
|
+
ready_time: AwareDatetime | None = Field(
|
|
17
|
+
validation_alias=AliasChoices("ready-time", "ready_time"),
|
|
18
|
+
serialization_alias="ready-time",
|
|
19
|
+
default=None,
|
|
20
|
+
)
|
|
21
|
+
data: dict | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChangesResult(BaseModel):
|
|
25
|
+
id: str
|
|
26
|
+
kind: str
|
|
27
|
+
summary: str
|
|
28
|
+
status: str
|
|
29
|
+
tasks: list[Task]
|
|
30
|
+
ready: bool
|
|
31
|
+
spawn_time: AwareDatetime = Field(
|
|
32
|
+
validation_alias=AliasChoices("spawn-time", "spawn_time"),
|
|
33
|
+
serialization_alias="spawn-time",
|
|
34
|
+
)
|
|
35
|
+
ready_time: AwareDatetime | None = Field(
|
|
36
|
+
validation_alias=AliasChoices("ready-time", "ready_time"),
|
|
37
|
+
serialization_alias="ready-time",
|
|
38
|
+
default=None,
|
|
39
|
+
)
|
|
40
|
+
err: str | None = None
|
|
41
|
+
data: dict | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ChangesResponse(BaseResponse):
|
|
45
|
+
result: ChangesResult | BaseErrorResult
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def ready(self) -> bool:
|
|
49
|
+
if isinstance(self.result, BaseErrorResult):
|
|
50
|
+
return False
|
|
51
|
+
return self.result.ready
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Literal, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseErrorResult(BaseModel):
|
|
8
|
+
message: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseResponse(BaseModel):
|
|
12
|
+
status_code: int = Field(
|
|
13
|
+
validation_alias=AliasChoices("status-code", "status_code"),
|
|
14
|
+
serialization_alias="status-code",
|
|
15
|
+
)
|
|
16
|
+
type: Literal["sync", "async", "error"]
|
|
17
|
+
status: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SnapBaseVersion(Enum):
|
|
21
|
+
core16 = "core16"
|
|
22
|
+
core18 = "core18"
|
|
23
|
+
core20 = "core20"
|
|
24
|
+
core22 = "core22"
|
|
25
|
+
core24 = "core24"
|
|
26
|
+
bare = "bare"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SnapConfinement(Enum):
|
|
30
|
+
strict = "strict"
|
|
31
|
+
classic = "classic"
|
|
32
|
+
devmode = "devmode"
|
|
33
|
+
jailmode = "jailmode"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SnapApp(BaseModel):
|
|
37
|
+
snap: str | None = None
|
|
38
|
+
name: str
|
|
39
|
+
desktop_file: str | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
validation_alias=AliasChoices("desktop-file", "desktop_file"),
|
|
42
|
+
serialization_alias="desktop-file",
|
|
43
|
+
)
|
|
44
|
+
daemon: str | None = None
|
|
45
|
+
enabled: bool | None = None
|
|
46
|
+
active: bool | None = None
|
|
47
|
+
common_id: str | None = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
validation_alias=AliasChoices("common-id", "common_id"),
|
|
50
|
+
serialization_alias="common-id",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Media(BaseModel):
|
|
55
|
+
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
56
|
+
|
|
57
|
+
height: Optional[float] = None
|
|
58
|
+
type: str
|
|
59
|
+
url: str
|
|
60
|
+
width: Optional[float] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AsyncResponse(BaseResponse):
|
|
64
|
+
change: str
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from pydantic import AliasChoices, AwareDatetime, BaseModel, ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
from snap_python.schemas.common import (
|
|
4
|
+
BaseResponse,
|
|
5
|
+
Media,
|
|
6
|
+
SnapApp,
|
|
7
|
+
SnapBaseVersion,
|
|
8
|
+
SnapConfinement,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Snap(BaseModel):
|
|
13
|
+
model_config = ConfigDict(extra="allow")
|
|
14
|
+
apps: list[SnapApp] = Field(default_factory=list)
|
|
15
|
+
base: SnapBaseVersion | None = None
|
|
16
|
+
channel: str
|
|
17
|
+
confinement: SnapConfinement
|
|
18
|
+
contact: str
|
|
19
|
+
description: str
|
|
20
|
+
developer: str
|
|
21
|
+
devmode: bool
|
|
22
|
+
icon: str | None = None
|
|
23
|
+
id: str
|
|
24
|
+
ignore_validation: bool = Field(
|
|
25
|
+
validation_alias=AliasChoices("ignore-validation", "ignore_validation"),
|
|
26
|
+
serialization_alias="ignore-validation",
|
|
27
|
+
)
|
|
28
|
+
install_date: AwareDatetime = Field(
|
|
29
|
+
validation_alias=AliasChoices("install-date", "install_date"),
|
|
30
|
+
serialization_alias="install-date",
|
|
31
|
+
)
|
|
32
|
+
installed_size: int = Field(
|
|
33
|
+
validation_alias=AliasChoices("installed-size", "installed_size"),
|
|
34
|
+
serialization_alias="installed-size",
|
|
35
|
+
)
|
|
36
|
+
jailmode: bool
|
|
37
|
+
license: str = "unset"
|
|
38
|
+
links: dict[str, list[str]] | None = None
|
|
39
|
+
media: list[Media] = Field(default_factory=list)
|
|
40
|
+
mounted_from: str = Field(
|
|
41
|
+
validation_alias=AliasChoices("mounted-from", "mounted_from"),
|
|
42
|
+
serialization_alias="mounted-from",
|
|
43
|
+
)
|
|
44
|
+
name: str
|
|
45
|
+
private: bool
|
|
46
|
+
publisher: dict[str, str]
|
|
47
|
+
revision: str
|
|
48
|
+
status: str
|
|
49
|
+
summary: str
|
|
50
|
+
title: str | None = None
|
|
51
|
+
tracking_channel: str = Field(
|
|
52
|
+
validation_alias=AliasChoices("tracking-channel", "tracking_channel"),
|
|
53
|
+
serialization_alias="tracking-channel",
|
|
54
|
+
)
|
|
55
|
+
type: str
|
|
56
|
+
version: str
|
|
57
|
+
website: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SnapListResponse(BaseResponse):
|
|
61
|
+
result: list[Snap]
|
|
62
|
+
|
|
63
|
+
def __len__(self):
|
|
64
|
+
return len(self.result)
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: category_schema.json
|
|
3
|
+
# timestamp: 2024-09-17T00:34:03+00:00
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
VALID_CATEGORY_FIELDS = ["name", "type", "title", "summary", "description", "media"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Media(BaseModel):
|
|
13
|
+
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
14
|
+
|
|
15
|
+
height: Optional[float] = None
|
|
16
|
+
type: str
|
|
17
|
+
url: str
|
|
18
|
+
width: Optional[float] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Category(BaseModel):
|
|
22
|
+
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
23
|
+
|
|
24
|
+
description: Optional[str] = None
|
|
25
|
+
media: Optional[Media] = None
|
|
26
|
+
name: Optional[str] = None
|
|
27
|
+
summary: Optional[str] = None
|
|
28
|
+
title: Optional[str] = None
|
|
29
|
+
type: Optional[str] = None
|
|
30
|
+
featured: Optional[bool] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CategoryResponse(BaseModel):
|
|
34
|
+
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
35
|
+
|
|
36
|
+
categories: Optional[List[Category]] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SingleCategoryResponse(BaseModel):
|
|
40
|
+
model_config = ConfigDict(extra="forbid", exclude_unset=True)
|
|
41
|
+
|
|
42
|
+
category: Category
|