snap-python 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snap_python/__init__.py +2 -2
- snap_python/client.py +166 -44
- snap_python/components/changes.py +157 -0
- snap_python/components/config.py +10 -13
- snap_python/components/interfaces.py +283 -0
- snap_python/components/logs.py +162 -0
- snap_python/components/snaps.py +1013 -159
- snap_python/components/store.py +64 -27
- snap_python/schemas/changes.py +21 -1
- snap_python/schemas/common.py +45 -8
- snap_python/schemas/interfaces.py +251 -0
- snap_python/schemas/logs.py +17 -0
- snap_python/schemas/snaps.py +116 -8
- snap_python/schemas/store/categories.py +4 -4
- snap_python/schemas/store/info.py +25 -8
- snap_python/schemas/store/refresh.py +5 -5
- snap_python/schemas/store/search.py +1 -7
- snap_python/schemas/store/track.py +71 -53
- snap_python/utils.py +185 -3
- {snap_python-0.1.6.dist-info → snap_python-0.2.0.dist-info}/METADATA +7 -5
- snap_python-0.2.0.dist-info/RECORD +31 -0
- {snap_python-0.1.6.dist-info → snap_python-0.2.0.dist-info}/WHEEL +1 -1
- snap_python-0.1.6.dist-info/RECORD +0 -26
- {snap_python-0.1.6.dist-info → snap_python-0.2.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
from snap_python.schemas.changes import ChangesResponse
|
|
5
|
+
from snap_python.schemas.common import AsyncResponse
|
|
6
|
+
from snap_python.schemas.interfaces import (
|
|
7
|
+
ConnectionsResponse,
|
|
8
|
+
InterfaceActionPlug,
|
|
9
|
+
InterfaceActionSlot,
|
|
10
|
+
InterfacesResponse,
|
|
11
|
+
)
|
|
12
|
+
from snap_python.utils import AbstractSnapsClient, wait_for_change
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("snap_python.components.interfaces")
|
|
15
|
+
|
|
16
|
+
#: Values ``GET /v2/interfaces`` accepts for ``select``. Sending no ``select``
|
|
17
|
+
#: at all selects the legacy object response shape.
|
|
18
|
+
InterfaceSelect = Literal["all", "connected"]
|
|
19
|
+
|
|
20
|
+
#: Values ``GET /v2/connections`` accepts for ``select``.
|
|
21
|
+
ConnectionsSelect = Literal["", "all"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InterfacesEndpoints:
|
|
25
|
+
"""Endpoints for interfaces and their connections.
|
|
26
|
+
|
|
27
|
+
:param client: Client used to talk to snapd.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, client: AbstractSnapsClient) -> None:
|
|
31
|
+
self._client = client
|
|
32
|
+
self.common_endpoint = "interfaces"
|
|
33
|
+
|
|
34
|
+
async def list_interfaces(
|
|
35
|
+
self,
|
|
36
|
+
select: InterfaceSelect | None = None,
|
|
37
|
+
plugs: bool = False,
|
|
38
|
+
slots: bool = False,
|
|
39
|
+
doc: bool = False,
|
|
40
|
+
names: list[str] | None = None,
|
|
41
|
+
) -> InterfacesResponse:
|
|
42
|
+
"""
|
|
43
|
+
List interfaces known to snapd.
|
|
44
|
+
|
|
45
|
+
snapd picks the response shape from ``select``: without it the legacy
|
|
46
|
+
handler answers with a ``{"plugs": [...], "slots": [...]}`` object,
|
|
47
|
+
with any non-empty value the modern handler answers with an array.
|
|
48
|
+
:class:`InterfacesResponse` accepts both;
|
|
49
|
+
:attr:`InterfacesResponse.legacy` tells them apart and
|
|
50
|
+
:attr:`InterfacesResponse.interfaces` normalises them.
|
|
51
|
+
|
|
52
|
+
The ``plugs``/``slots``/``doc`` flags are only honoured by the modern
|
|
53
|
+
handler, and only when exactly ``true``; they are not sent unless
|
|
54
|
+
requested.
|
|
55
|
+
|
|
56
|
+
:param select: ``"all"`` or ``"connected"`` (the latter keeps only
|
|
57
|
+
interfaces with at least one live connection). None (default)
|
|
58
|
+
requests the legacy response shape.
|
|
59
|
+
:type select: str, optional
|
|
60
|
+
:param plugs: Include the ``plugs`` arrays. Only for the modern shape.
|
|
61
|
+
:type plugs: bool
|
|
62
|
+
:param slots: Include the ``slots`` arrays. Only for the modern shape.
|
|
63
|
+
:type slots: bool
|
|
64
|
+
:param doc: Include ``doc-url``, defaulting to
|
|
65
|
+
``https://snapcraft.io/docs/<name>-interface``. Only for the modern
|
|
66
|
+
shape.
|
|
67
|
+
:type doc: bool
|
|
68
|
+
:param names: Restrict the result to these interface names. None
|
|
69
|
+
(default) returns every interface. Names are joined with commas and
|
|
70
|
+
snapd does not trim whitespace.
|
|
71
|
+
:type names: list[str], optional
|
|
72
|
+
|
|
73
|
+
:returns: The response object containing the interfaces.
|
|
74
|
+
:rtype: InterfacesResponse
|
|
75
|
+
|
|
76
|
+
:raises SnapdAPIError: If snapd rejects the request, for example for an
|
|
77
|
+
unknown ``select`` value.
|
|
78
|
+
:raises httpx.HTTPStatusError: If the request results in an HTTP error.
|
|
79
|
+
"""
|
|
80
|
+
params: dict[str, str] = {}
|
|
81
|
+
if select is not None:
|
|
82
|
+
params["select"] = select
|
|
83
|
+
if plugs:
|
|
84
|
+
params["plugs"] = "true"
|
|
85
|
+
if slots:
|
|
86
|
+
params["slots"] = "true"
|
|
87
|
+
if doc:
|
|
88
|
+
params["doc"] = "true"
|
|
89
|
+
if names:
|
|
90
|
+
params["names"] = ",".join(names)
|
|
91
|
+
|
|
92
|
+
response = await self._client.request(
|
|
93
|
+
"GET", self.common_endpoint, params=params
|
|
94
|
+
)
|
|
95
|
+
return InterfacesResponse.model_validate_json(response.content)
|
|
96
|
+
|
|
97
|
+
async def connections(
|
|
98
|
+
self,
|
|
99
|
+
snap: str | None = None,
|
|
100
|
+
select: ConnectionsSelect | None = None,
|
|
101
|
+
interface: str | None = None,
|
|
102
|
+
) -> ConnectionsResponse:
|
|
103
|
+
"""
|
|
104
|
+
Retrieve plug/slot connections.
|
|
105
|
+
|
|
106
|
+
:param snap: Only return connections involving this snap. snapd also
|
|
107
|
+
keeps the counterpart plug/slot of another snap and everything
|
|
108
|
+
connected to it. None (default) returns every snap. An unknown snap
|
|
109
|
+
is a 404 ``snap-not-found``.
|
|
110
|
+
:type snap: str, optional
|
|
111
|
+
:param select: ``"all"`` also returns unconnected plugs/slots and
|
|
112
|
+
undesired connections; ``""`` (daemon default when the parameter is
|
|
113
|
+
omitted) returns only established connections. None (default) omits
|
|
114
|
+
the parameter.
|
|
115
|
+
:type select: str, optional
|
|
116
|
+
:param interface: Only return connections, plugs and slots of this
|
|
117
|
+
interface. An unknown interface yields an empty result.
|
|
118
|
+
:type interface: str, optional
|
|
119
|
+
|
|
120
|
+
:returns: The response object containing connections, plugs and slots.
|
|
121
|
+
:rtype: ConnectionsResponse
|
|
122
|
+
|
|
123
|
+
:raises SnapdAPIError: If snapd rejects the request, for example for an
|
|
124
|
+
unknown ``select`` value or an unknown snap (404).
|
|
125
|
+
:raises httpx.HTTPStatusError: If the request results in an HTTP error.
|
|
126
|
+
"""
|
|
127
|
+
params: dict[str, str] = {}
|
|
128
|
+
if snap is not None:
|
|
129
|
+
params["snap"] = snap
|
|
130
|
+
if select:
|
|
131
|
+
params["select"] = select
|
|
132
|
+
if interface is not None:
|
|
133
|
+
params["interface"] = interface
|
|
134
|
+
|
|
135
|
+
response = await self._client.request("GET", "connections", params=params)
|
|
136
|
+
return ConnectionsResponse.model_validate_json(response.content)
|
|
137
|
+
|
|
138
|
+
async def _change_interface(
|
|
139
|
+
self,
|
|
140
|
+
action: Literal["connect", "disconnect"],
|
|
141
|
+
plug_snap: str,
|
|
142
|
+
plug_name: str,
|
|
143
|
+
slot_snap: str | None,
|
|
144
|
+
slot_name: str | None,
|
|
145
|
+
forget: bool,
|
|
146
|
+
wait: bool,
|
|
147
|
+
timeout: float | None,
|
|
148
|
+
poll_interval: float,
|
|
149
|
+
) -> AsyncResponse | ChangesResponse:
|
|
150
|
+
"""POST one plug/slot pair to ``/v2/interfaces``.
|
|
151
|
+
|
|
152
|
+
snapd rejects more than one plug or slot per request with 501
|
|
153
|
+
``many-to-many operations are not implemented``, so the API always
|
|
154
|
+
sends exactly one of each. ``forget`` is only ever set by
|
|
155
|
+
:meth:`disconnect`.
|
|
156
|
+
"""
|
|
157
|
+
body: dict[str, Any] = {
|
|
158
|
+
"action": action,
|
|
159
|
+
"plugs": [InterfaceActionPlug(snap=plug_snap, plug=plug_name).model_dump()],
|
|
160
|
+
"slots": [
|
|
161
|
+
InterfaceActionSlot(
|
|
162
|
+
snap=slot_snap or "", slot=slot_name or ""
|
|
163
|
+
).model_dump()
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
if forget:
|
|
167
|
+
body["forget"] = True
|
|
168
|
+
|
|
169
|
+
response = await self._client.request("POST", self.common_endpoint, json=body)
|
|
170
|
+
async_response = AsyncResponse.model_validate_json(response.content)
|
|
171
|
+
if not wait:
|
|
172
|
+
return async_response
|
|
173
|
+
return await wait_for_change(
|
|
174
|
+
self._client,
|
|
175
|
+
async_response.change,
|
|
176
|
+
timeout=timeout,
|
|
177
|
+
poll_interval=poll_interval,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def connect(
|
|
181
|
+
self,
|
|
182
|
+
plug_snap: str,
|
|
183
|
+
plug_name: str,
|
|
184
|
+
slot_snap: str | None = None,
|
|
185
|
+
slot_name: str | None = None,
|
|
186
|
+
wait: bool = True,
|
|
187
|
+
timeout: float | None = None,
|
|
188
|
+
poll_interval: float = 0.5,
|
|
189
|
+
) -> AsyncResponse | ChangesResponse:
|
|
190
|
+
"""
|
|
191
|
+
Connect one plug to one slot.
|
|
192
|
+
|
|
193
|
+
An empty ``slot_snap`` lets snapd fall back to ``snapd``, ``core`` or
|
|
194
|
+
``ubuntu-core``; an empty ``slot_name`` lets it pick the single slot of
|
|
195
|
+
the plug's interface. Connecting an already connected pair still
|
|
196
|
+
returns an async response, but with a change that is already done.
|
|
197
|
+
|
|
198
|
+
:param plug_snap: Snap providing the plug.
|
|
199
|
+
:type plug_snap: str
|
|
200
|
+
:param plug_name: Name of the plug.
|
|
201
|
+
:type plug_name: str
|
|
202
|
+
:param slot_snap: Snap providing the slot, or None to let snapd choose.
|
|
203
|
+
:type slot_snap: str, optional
|
|
204
|
+
:param slot_name: Name of the slot, or None to let snapd choose.
|
|
205
|
+
:type slot_name: str, optional
|
|
206
|
+
:param wait: Wait for the connect change to finish before returning.
|
|
207
|
+
:type wait: bool
|
|
208
|
+
:param timeout: Maximum seconds to wait for the change when ``wait`` is
|
|
209
|
+
True; None waits forever.
|
|
210
|
+
:type timeout: float | None
|
|
211
|
+
:param poll_interval: Seconds between two polls while waiting.
|
|
212
|
+
:type poll_interval: float
|
|
213
|
+
|
|
214
|
+
:returns: The async response, or the finished change when waiting.
|
|
215
|
+
:rtype: AsyncResponse | ChangesResponse
|
|
216
|
+
|
|
217
|
+
:raises SnapdAPIError: If snapd refuses the connection, or the change
|
|
218
|
+
fails.
|
|
219
|
+
:raises httpx.HTTPStatusError: If the request results in an HTTP error.
|
|
220
|
+
"""
|
|
221
|
+
return await self._change_interface(
|
|
222
|
+
"connect",
|
|
223
|
+
plug_snap,
|
|
224
|
+
plug_name,
|
|
225
|
+
slot_snap,
|
|
226
|
+
slot_name,
|
|
227
|
+
forget=False,
|
|
228
|
+
wait=wait,
|
|
229
|
+
timeout=timeout,
|
|
230
|
+
poll_interval=poll_interval,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def disconnect(
|
|
234
|
+
self,
|
|
235
|
+
plug_snap: str,
|
|
236
|
+
plug_name: str,
|
|
237
|
+
slot_snap: str | None = None,
|
|
238
|
+
slot_name: str | None = None,
|
|
239
|
+
forget: bool = False,
|
|
240
|
+
wait: bool = True,
|
|
241
|
+
timeout: float | None = None,
|
|
242
|
+
poll_interval: float = 0.5,
|
|
243
|
+
) -> AsyncResponse | ChangesResponse:
|
|
244
|
+
"""
|
|
245
|
+
Disconnect one plug from one slot.
|
|
246
|
+
|
|
247
|
+
:param plug_snap: Snap providing the plug.
|
|
248
|
+
:type plug_snap: str
|
|
249
|
+
:param plug_name: Name of the plug.
|
|
250
|
+
:type plug_name: str
|
|
251
|
+
:param slot_snap: Snap providing the slot, or None to let snapd choose.
|
|
252
|
+
:type slot_snap: str, optional
|
|
253
|
+
:param slot_name: Name of the slot, or None to let snapd choose.
|
|
254
|
+
:type slot_name: str, optional
|
|
255
|
+
:param forget: Drop the connection state instead of recording it as
|
|
256
|
+
undesired, so a later refresh does not reconnect the pair.
|
|
257
|
+
:type forget: bool
|
|
258
|
+
:param wait: Wait for the disconnect change to finish before returning.
|
|
259
|
+
:type wait: bool
|
|
260
|
+
:param timeout: Maximum seconds to wait for the change when ``wait`` is
|
|
261
|
+
True; None waits forever.
|
|
262
|
+
:type timeout: float | None
|
|
263
|
+
:param poll_interval: Seconds between two polls while waiting.
|
|
264
|
+
:type poll_interval: float
|
|
265
|
+
|
|
266
|
+
:returns: The async response, or the finished change when waiting.
|
|
267
|
+
:rtype: AsyncResponse | ChangesResponse
|
|
268
|
+
|
|
269
|
+
:raises SnapdAPIError: If snapd refuses the disconnection, for example
|
|
270
|
+
because the pair is not connected, or the change fails.
|
|
271
|
+
:raises httpx.HTTPStatusError: If the request results in an HTTP error.
|
|
272
|
+
"""
|
|
273
|
+
return await self._change_interface(
|
|
274
|
+
"disconnect",
|
|
275
|
+
plug_snap,
|
|
276
|
+
plug_name,
|
|
277
|
+
slot_snap,
|
|
278
|
+
slot_name,
|
|
279
|
+
forget=forget,
|
|
280
|
+
wait=wait,
|
|
281
|
+
timeout=timeout,
|
|
282
|
+
poll_interval=poll_interval,
|
|
283
|
+
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import AsyncGenerator
|
|
3
|
+
|
|
4
|
+
from snap_python.schemas.logs import LogEntry
|
|
5
|
+
from snap_python.utils import AbstractSnapsClient
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("snap_python.components.logs")
|
|
8
|
+
|
|
9
|
+
#: RFC 7464 record separator that prefixes every JSON-seq record.
|
|
10
|
+
RECORD_SEPARATOR = "\x1e"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_record(line: bytes) -> LogEntry | None:
|
|
14
|
+
"""Parse one JSON-seq line, returning None for anything unusable.
|
|
15
|
+
|
|
16
|
+
Records look like ``<RS>{"timestamp": ...}\\n``. Truncated, empty or
|
|
17
|
+
non-JSON records are skipped, exactly like snapd's own client. That also
|
|
18
|
+
covers the raw ``\\x1E{"error": ...}`` record snapd writes when the journal
|
|
19
|
+
read fails mid-stream: it has no real RS byte, so it is never JSON-seq.
|
|
20
|
+
"""
|
|
21
|
+
text = line.decode("utf-8", errors="replace")
|
|
22
|
+
index = text.find(RECORD_SEPARATOR)
|
|
23
|
+
if index < 0:
|
|
24
|
+
return None
|
|
25
|
+
payload = text[index + 1 :].strip()
|
|
26
|
+
if not payload:
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
return LogEntry.model_validate_json(payload)
|
|
30
|
+
except ValueError:
|
|
31
|
+
logger.debug("Skipping malformed log record: %r", text)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LogsEndpoints:
|
|
36
|
+
"""Endpoint for ``/v2/logs``, a JSON-seq journal stream.
|
|
37
|
+
|
|
38
|
+
``GET /v2/logs`` does not answer with the usual JSON envelope: it streams
|
|
39
|
+
RFC 7464 records, each a record separator byte (``0x1E``), one JSON object
|
|
40
|
+
and a newline. Errors raised before the stream starts are ordinary snapd
|
|
41
|
+
error envelopes.
|
|
42
|
+
|
|
43
|
+
Records are split on LF only. Neither :meth:`str.splitlines` nor httpx's
|
|
44
|
+
``aiter_lines`` may be used here: both treat the RS byte itself as a line
|
|
45
|
+
boundary, which would shred every record.
|
|
46
|
+
|
|
47
|
+
:param client: Client used to talk to snapd.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, client: AbstractSnapsClient) -> None:
|
|
51
|
+
self._client = client
|
|
52
|
+
self.common_endpoint = "logs"
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _params(
|
|
56
|
+
names: list[str] | None, n: int | None, follow: bool = False
|
|
57
|
+
) -> dict[str, str]:
|
|
58
|
+
"""Build the query parameters of ``GET /v2/logs``."""
|
|
59
|
+
params: dict[str, str] = {}
|
|
60
|
+
if names:
|
|
61
|
+
params["names"] = ",".join(names)
|
|
62
|
+
if n is not None:
|
|
63
|
+
params["n"] = str(n)
|
|
64
|
+
if follow:
|
|
65
|
+
params["follow"] = "true"
|
|
66
|
+
return params
|
|
67
|
+
|
|
68
|
+
async def get_logs(
|
|
69
|
+
self,
|
|
70
|
+
names: list[str] | None = None,
|
|
71
|
+
n: int | None = None,
|
|
72
|
+
) -> list[LogEntry]:
|
|
73
|
+
"""
|
|
74
|
+
Retrieve a bounded batch of log entries.
|
|
75
|
+
|
|
76
|
+
:param names: Services to read logs from, as ``"<snap>"`` or
|
|
77
|
+
``"<snap>.<app>"`` entries. Only services have logs. None (default)
|
|
78
|
+
reads every service snapd can find.
|
|
79
|
+
:type names: list[str], optional
|
|
80
|
+
:param n: Number of lines snapd returns initially. ``-1`` means every
|
|
81
|
+
line; None (default) omits the parameter, so snapd uses its own
|
|
82
|
+
default of 10.
|
|
83
|
+
:type n: int, optional
|
|
84
|
+
|
|
85
|
+
:returns: The parsed log entries, in stream order.
|
|
86
|
+
:rtype: list[LogEntry]
|
|
87
|
+
|
|
88
|
+
:raises SnapdAPIError: If snapd answers with an error envelope, for
|
|
89
|
+
example 404 when a name is not a service snap.
|
|
90
|
+
:raises httpx.HTTPStatusError: If the request results in an HTTP error.
|
|
91
|
+
"""
|
|
92
|
+
response = await self._client.request(
|
|
93
|
+
"GET", self.common_endpoint, params=self._params(names, n)
|
|
94
|
+
)
|
|
95
|
+
entries = (_parse_record(line) for line in response.content.split(b"\n"))
|
|
96
|
+
return [entry for entry in entries if entry is not None]
|
|
97
|
+
|
|
98
|
+
async def stream_logs(
|
|
99
|
+
self,
|
|
100
|
+
names: list[str] | None = None,
|
|
101
|
+
n: int | None = None,
|
|
102
|
+
) -> AsyncGenerator[LogEntry, None]:
|
|
103
|
+
"""
|
|
104
|
+
Follow the log stream, yielding entries as snapd writes them.
|
|
105
|
+
|
|
106
|
+
``follow=true`` never terminates on its own, so this is an async
|
|
107
|
+
generator: break out of the ``async for`` to close the stream. Unlike
|
|
108
|
+
:meth:`get_logs` it needs the underlying ``httpx`` client, because
|
|
109
|
+
:meth:`snap_python.client.SnapClient.request` buffers the whole body.
|
|
110
|
+
Malformed records are skipped rather than raised on.
|
|
111
|
+
|
|
112
|
+
:param names: Services to read logs from, as ``"<snap>"`` or
|
|
113
|
+
``"<snap>.app"`` entries. None (default) reads every service.
|
|
114
|
+
:type names: list[str], optional
|
|
115
|
+
:param n: Number of lines snapd replays before following. ``-1`` means
|
|
116
|
+
every line; None (default) keeps snapd's default of 10.
|
|
117
|
+
:type n: int, optional
|
|
118
|
+
|
|
119
|
+
:yields: One parsed entry per record, in stream order.
|
|
120
|
+
:rtype: LogEntry
|
|
121
|
+
|
|
122
|
+
:raises SnapdAPIError: If snapd answers with an error envelope instead
|
|
123
|
+
of a stream, for example 404 for an unknown service.
|
|
124
|
+
:raises httpx.HTTPStatusError: If the request results in an HTTP error
|
|
125
|
+
that is not a snapd envelope.
|
|
126
|
+
:raises NotImplementedError: If the client does not expose the raw
|
|
127
|
+
``httpx`` client and its URL builder.
|
|
128
|
+
"""
|
|
129
|
+
snapd_client = getattr(self._client, "snapd_client", None)
|
|
130
|
+
build_url = getattr(self._client, "endpoint_url", None)
|
|
131
|
+
if snapd_client is None or build_url is None:
|
|
132
|
+
raise NotImplementedError(
|
|
133
|
+
"log streaming needs a client exposing 'snapd_client' and "
|
|
134
|
+
f"'endpoint_url()'; {type(self._client).__name__} is not usable"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
params = self._params(names, n, follow=True)
|
|
138
|
+
async with snapd_client.stream(
|
|
139
|
+
"GET", build_url(self.common_endpoint), params=params
|
|
140
|
+
) as response:
|
|
141
|
+
if response.is_error:
|
|
142
|
+
await response.aread()
|
|
143
|
+
# Imported lazily: snap_python.client imports this module.
|
|
144
|
+
from snap_python.client import _snapd_error_from_response
|
|
145
|
+
|
|
146
|
+
snapd_error = _snapd_error_from_response(response)
|
|
147
|
+
if snapd_error is not None:
|
|
148
|
+
raise snapd_error
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
|
|
151
|
+
buffer = b""
|
|
152
|
+
async for chunk in response.aiter_bytes():
|
|
153
|
+
buffer += chunk
|
|
154
|
+
while b"\n" in buffer:
|
|
155
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
156
|
+
entry = _parse_record(line)
|
|
157
|
+
if entry is not None:
|
|
158
|
+
yield entry
|
|
159
|
+
if buffer:
|
|
160
|
+
entry = _parse_record(buffer)
|
|
161
|
+
if entry is not None:
|
|
162
|
+
yield entry
|