snap-python 0.0.1__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/__init__.py +0 -0
- snap_python/client.py +86 -0
- snap_python/components/__init__.py +0 -0
- snap_python/components/snaps.py +92 -0
- snap_python/components/store.py +167 -0
- snap_python/schemas/changes.py +51 -0
- snap_python/schemas/common.py +64 -0
- snap_python/schemas/snaps.py +64 -0
- snap_python/schemas/store/__init__.py +0 -0
- snap_python/schemas/store/categories.py +42 -0
- snap_python/schemas/store/info.py +134 -0
- snap_python/schemas/store/json/category_by_name_schema.json +61 -0
- snap_python/schemas/store/json/category_schema.json +61 -0
- snap_python/schemas/store/json/snaps_find_schema.json +221 -0
- snap_python/schemas/store/json/snaps_info_schema.json +432 -0
- snap_python/schemas/store/search.py +160 -0
- snap_python/utils.py +19 -0
- snap_python-0.0.1.dist-info/LICENSE +21 -0
- snap_python-0.0.1.dist-info/METADATA +39 -0
- snap_python-0.0.1.dist-info/RECORD +21 -0
- snap_python-0.0.1.dist-info/WHEEL +4 -0
snap_python/__init__.py
ADDED
|
File without changes
|
snap_python/client.py
ADDED
|
@@ -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
|