pieforhead 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.
- pieforhead/__init__.py +4 -0
- pieforhead/clients.py +481 -0
- pieforhead/exceptions.py +14 -0
- pieforhead/image.py +185 -0
- pieforhead/pieforhead.py +79 -0
- pieforhead/site.py +142 -0
- pieforhead/utils.py +1 -0
- pieforhead-0.0.1.dist-info/METADATA +119 -0
- pieforhead-0.0.1.dist-info/RECORD +11 -0
- pieforhead-0.0.1.dist-info/WHEEL +4 -0
- pieforhead-0.0.1.dist-info/licenses/LICENSE +24 -0
pieforhead/__init__.py
ADDED
pieforhead/clients.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import semver
|
|
6
|
+
|
|
7
|
+
from .exceptions import PieFedClientException
|
|
8
|
+
from .utils import __version__ as VERSION
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseClient:
|
|
15
|
+
"""Class that both synchronous and async clients inherit from."""
|
|
16
|
+
|
|
17
|
+
instance_url: str
|
|
18
|
+
timeout: float = 5.0
|
|
19
|
+
api_version: str = "alpha"
|
|
20
|
+
headers: dict = {}
|
|
21
|
+
jwt_token: str = ""
|
|
22
|
+
current_username_or_email: str = ""
|
|
23
|
+
current_password: str = ""
|
|
24
|
+
raise_exceptions: bool = True
|
|
25
|
+
_nodeinfo: dict | None = None
|
|
26
|
+
_instance_version: semver.Version | None = None
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
instance_url: str,
|
|
31
|
+
timeout: float = 5.0,
|
|
32
|
+
api_version: str = "alpha",
|
|
33
|
+
raise_exceptions: bool = True,
|
|
34
|
+
):
|
|
35
|
+
"""Constructor for base client class.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
instance_url str: url of the piefed instance e.g. https://site.tld
|
|
39
|
+
timeout (float, optional): Timeout value for requests. Defaults to 5.0.
|
|
40
|
+
api_version (str, optional): Version of the PieFed API. Defaults to "alpha".
|
|
41
|
+
"""
|
|
42
|
+
self.instance_url = instance_url
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
self.api_version = api_version
|
|
45
|
+
self.raise_exceptions = raise_exceptions
|
|
46
|
+
self.headers = {"User-Agent": f"pieforhead/{VERSION}", "Accept": "application/json"}
|
|
47
|
+
|
|
48
|
+
def _request_error(self, message: str, error: Exception) -> dict | None:
|
|
49
|
+
# Always log the error
|
|
50
|
+
logger.error(message)
|
|
51
|
+
|
|
52
|
+
# Only raise if desired
|
|
53
|
+
if self.raise_exceptions:
|
|
54
|
+
raise PieFedClientException(message) from error
|
|
55
|
+
|
|
56
|
+
# Return None if raising is not desired
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PieFedClient(BaseClient):
|
|
61
|
+
"""Synchronous client for handling network requests."""
|
|
62
|
+
|
|
63
|
+
_client: httpx.Client | None = None
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
instance_url: str,
|
|
68
|
+
timeout: float = 5.0,
|
|
69
|
+
api_version: str = "alpha",
|
|
70
|
+
existing_client: httpx.Client | None = None,
|
|
71
|
+
raise_exceptions: bool = True,
|
|
72
|
+
):
|
|
73
|
+
"""Constructor for Synchronous client.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
instance_url str: url of the piefed instance e.g. https://site.tld
|
|
77
|
+
timeout (float, optional): Timeout value for requests. Defaults to 5.0.
|
|
78
|
+
api_version (str, optional): Version of the PieFed API. Defaults to "alpha".
|
|
79
|
+
existing_client (httpx.Client, optional): Pass an httpx.Client object to reuse it (mainly used for tests).
|
|
80
|
+
Defaults to None.
|
|
81
|
+
raise_exceptions (bool, optional): Whether to raise exceptions on request errors or just return None.
|
|
82
|
+
Defaults to True.
|
|
83
|
+
"""
|
|
84
|
+
super().__init__(
|
|
85
|
+
instance_url=instance_url,
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
api_version=api_version,
|
|
88
|
+
raise_exceptions=raise_exceptions,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
base_url = f"{instance_url}/api/{api_version}/"
|
|
92
|
+
|
|
93
|
+
if existing_client:
|
|
94
|
+
self._client = existing_client
|
|
95
|
+
else:
|
|
96
|
+
self._client = httpx.Client(headers=self.headers, timeout=self.timeout, base_url=base_url)
|
|
97
|
+
|
|
98
|
+
def request(
|
|
99
|
+
self,
|
|
100
|
+
method: str,
|
|
101
|
+
endpoint: str,
|
|
102
|
+
params: dict = {},
|
|
103
|
+
data: dict = {},
|
|
104
|
+
files: dict | None = None,
|
|
105
|
+
) -> dict | None:
|
|
106
|
+
"""Function to handle network requests.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
method (str): HTTP method for the request, one of ["GET", "POST", "PUT"]
|
|
110
|
+
endpoint (str): API endpoint to send request to e.g. "post/list"
|
|
111
|
+
params (dict, optional): url parameters for the request (only for "GET") Defaults to {}.
|
|
112
|
+
data (dict, optional): json payload for the request ("POST" and "PUT") Defaults to {}.
|
|
113
|
+
files (dict, optional): multipart files to include with the request.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
PieFedClientException: should also include error message returned from server if available
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
dict: deserialized json returned by the server
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
if self.jwt_token and "Authorization" not in self.headers:
|
|
123
|
+
self.headers["Authorization"] = "Bearer " + self.jwt_token
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Make the request
|
|
127
|
+
logger.debug(f"method={method}, endpoint={endpoint}, params={params}, data={data}")
|
|
128
|
+
|
|
129
|
+
resp = self._client.request(
|
|
130
|
+
method=method,
|
|
131
|
+
url=endpoint,
|
|
132
|
+
headers=self.headers,
|
|
133
|
+
timeout=self.timeout,
|
|
134
|
+
params=params,
|
|
135
|
+
json=data,
|
|
136
|
+
files=files,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
|
|
141
|
+
# Everything went well
|
|
142
|
+
return resp.json()
|
|
143
|
+
|
|
144
|
+
except httpx.HTTPStatusError as e:
|
|
145
|
+
# Server returned a bad status code, try to pull out the returned error message
|
|
146
|
+
logger.error(msg=str(e))
|
|
147
|
+
|
|
148
|
+
message = f"{method} request to {endpoint} failed with status {resp.status_code}"
|
|
149
|
+
try:
|
|
150
|
+
# A try...except inside a try...except kinda sucks - but more information is better, right?
|
|
151
|
+
error_body = resp.json()
|
|
152
|
+
except json.decoder.JSONDecodeError:
|
|
153
|
+
error_body = {}
|
|
154
|
+
|
|
155
|
+
if error_body and error_body.get("message", ""):
|
|
156
|
+
message += f": {error_body['message']}"
|
|
157
|
+
|
|
158
|
+
return self._request_error(message=message, error=e)
|
|
159
|
+
|
|
160
|
+
except httpx.TimeoutException as e:
|
|
161
|
+
# Server timed out
|
|
162
|
+
logger.error(msg=str(e))
|
|
163
|
+
|
|
164
|
+
message = f"{method} request to {endpoint} timed out"
|
|
165
|
+
return self._request_error(message=message, error=e)
|
|
166
|
+
|
|
167
|
+
except httpx.RequestError as e:
|
|
168
|
+
# Some other problem with the request
|
|
169
|
+
logger.error(msg=str(e))
|
|
170
|
+
|
|
171
|
+
message = f"{method} request to {endpoint} failed"
|
|
172
|
+
return self._request_error(message=message, error=e)
|
|
173
|
+
|
|
174
|
+
except json.decoder.JSONDecodeError as e:
|
|
175
|
+
# Server returned something that isn't JSON
|
|
176
|
+
logger.error(msg=str(e))
|
|
177
|
+
|
|
178
|
+
message = f"{method} request to {endpoint} did not return parseable JSON"
|
|
179
|
+
return self._request_error(message=message, error=e)
|
|
180
|
+
|
|
181
|
+
def close(self):
|
|
182
|
+
self._client.close()
|
|
183
|
+
|
|
184
|
+
def log_in(self, username_or_email: str, password: str) -> bool:
|
|
185
|
+
payload = {"username": username_or_email, "password": password}
|
|
186
|
+
method = "POST"
|
|
187
|
+
|
|
188
|
+
resp = self.request(method=method, endpoint="user/login", data=payload)
|
|
189
|
+
|
|
190
|
+
if resp is None:
|
|
191
|
+
# Problem with the request that was suppressed, just return False
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
self.jwt_token = resp["jwt"]
|
|
195
|
+
self.headers["Authorization"] = "Bearer " + self.jwt_token
|
|
196
|
+
self.current_username_or_email = username_or_email
|
|
197
|
+
self.current_password = password
|
|
198
|
+
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def is_logged_in(self) -> bool:
|
|
202
|
+
return bool(self.jwt_token)
|
|
203
|
+
|
|
204
|
+
def log_out(self) -> bool:
|
|
205
|
+
method = "POST"
|
|
206
|
+
|
|
207
|
+
resp = self.request(method=method, endpoint="user/logout")
|
|
208
|
+
|
|
209
|
+
if resp is None:
|
|
210
|
+
# Problem with the request that was suppressed, just return False
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
self.jwt_token = ""
|
|
214
|
+
self.current_username_or_email = ""
|
|
215
|
+
self.current_password = ""
|
|
216
|
+
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def nodeinfo(self) -> dict | None:
|
|
221
|
+
"""Dict containing the instance's nodeinfo output.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
dict: deserialized copy of the instance's nodeinfo or None if the request failed and exception ignored
|
|
225
|
+
"""
|
|
226
|
+
if self._nodeinfo:
|
|
227
|
+
return self._nodeinfo
|
|
228
|
+
|
|
229
|
+
method = "GET"
|
|
230
|
+
endpoint = self.instance_url + "/nodeinfo/2.1.json"
|
|
231
|
+
|
|
232
|
+
resp = self.request(method=method, endpoint=endpoint)
|
|
233
|
+
self._nodeinfo = resp
|
|
234
|
+
|
|
235
|
+
return self._nodeinfo if self._nodeinfo else None
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def instance_version(self) -> semver.Version | None:
|
|
239
|
+
"""The instance's software version from the nodeinfo.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
semver.Version: software version of the piefed instance or None if something failed and the exception was
|
|
243
|
+
suppressed
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
current_nodeinfo = self.nodeinfo
|
|
247
|
+
|
|
248
|
+
if current_nodeinfo is None:
|
|
249
|
+
# Problem with the request that was suppressed, just return False
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
version_str = self.nodeinfo["software"]["version"]
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
self._instance_version = semver.Version.parse(version_str)
|
|
256
|
+
return self._instance_version
|
|
257
|
+
except ValueError as e:
|
|
258
|
+
logger.error(msg=str(e))
|
|
259
|
+
logger.error(f"_instance_version={version_str}")
|
|
260
|
+
|
|
261
|
+
message = f"Instance version from nodeinfo not parseable: {version_str}"
|
|
262
|
+
return self._request_error(message=message, error=e)
|
|
263
|
+
|
|
264
|
+
def __enter__(self):
|
|
265
|
+
return self
|
|
266
|
+
|
|
267
|
+
def __exit__(self, *args):
|
|
268
|
+
self.close()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class AsyncPieFedClient(BaseClient):
|
|
272
|
+
"""Asynchronous client for handling network requests."""
|
|
273
|
+
|
|
274
|
+
_client: httpx.AsyncClient = None
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
instance_url: str,
|
|
279
|
+
timeout: float = 5.0,
|
|
280
|
+
api_version: str = "alpha",
|
|
281
|
+
existing_client: httpx.AsyncClient | None = None,
|
|
282
|
+
raise_exceptions: bool = True,
|
|
283
|
+
):
|
|
284
|
+
"""Constructor for Asynchronous client.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
instance_url str: url of the piefed instance e.g. https://site.tld
|
|
288
|
+
timeout (float, optional): Timeout value for requests. Defaults to 5.0.
|
|
289
|
+
api_version (str, optional): Version of the PieFed API. Defaults to "alpha".
|
|
290
|
+
existing_client (httpx.Client, optional): Pass an httpx.Client object to reuse it (mainly used for tests).
|
|
291
|
+
Defaults to None.
|
|
292
|
+
raise_exceptions: (bool, optional): Whether to raise exceptions on request errors or just return None.
|
|
293
|
+
Defaults to True.
|
|
294
|
+
"""
|
|
295
|
+
super().__init__(
|
|
296
|
+
instance_url=instance_url,
|
|
297
|
+
timeout=timeout,
|
|
298
|
+
api_version=api_version,
|
|
299
|
+
raise_exceptions=raise_exceptions,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
base_url = f"{instance_url}/api/{api_version}/"
|
|
303
|
+
|
|
304
|
+
if existing_client:
|
|
305
|
+
self._client = existing_client
|
|
306
|
+
else:
|
|
307
|
+
self._client = httpx.AsyncClient(headers=self.headers, timeout=self.timeout, base_url=base_url)
|
|
308
|
+
|
|
309
|
+
async def request(
|
|
310
|
+
self,
|
|
311
|
+
method: str,
|
|
312
|
+
endpoint: str,
|
|
313
|
+
params: dict = {},
|
|
314
|
+
data: dict = {},
|
|
315
|
+
files: dict | None = None,
|
|
316
|
+
) -> dict | None:
|
|
317
|
+
"""Async function to handle network requests.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
method (str): HTTP method for the request, one of ["GET", "POST", "PUT"]
|
|
321
|
+
endpoint (str): API endpoint to send request to e.g. "post/list"
|
|
322
|
+
params (dict, optional): url parameters for the request (only for "GET") Defaults to {}.
|
|
323
|
+
data (dict, optional): json payload for the request ("POST" and "PUT") Defaults to {}.
|
|
324
|
+
files (dict, optional): multipart files to include with the request.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
PieFedClientException: should also include error message returned from server if available
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
dict: deserialized json returned by the server
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
if self.jwt_token and "Authorization" not in self.headers:
|
|
334
|
+
self.headers["Authorization"] = "Bearer " + self.jwt_token
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
# Make the request
|
|
338
|
+
logger.debug(f"method={method}, endpoint={endpoint}, params={params}, data={data}")
|
|
339
|
+
|
|
340
|
+
resp = await self._client.request(
|
|
341
|
+
method=method,
|
|
342
|
+
url=endpoint,
|
|
343
|
+
headers=self.headers,
|
|
344
|
+
timeout=self.timeout,
|
|
345
|
+
params=params,
|
|
346
|
+
json=data,
|
|
347
|
+
files=files,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
resp.raise_for_status()
|
|
351
|
+
|
|
352
|
+
# Everything went well
|
|
353
|
+
return resp.json()
|
|
354
|
+
|
|
355
|
+
except httpx.HTTPStatusError as e:
|
|
356
|
+
# Server returned a bad status code, try to pull out the returned error message
|
|
357
|
+
logger.error(msg=str(e))
|
|
358
|
+
|
|
359
|
+
message = f"{method} request to {endpoint} failed with status {resp.status_code}"
|
|
360
|
+
try:
|
|
361
|
+
# A try...except inside a try...except kinda sucks - but more information is better, right?
|
|
362
|
+
error_body = resp.json()
|
|
363
|
+
except json.decoder.JSONDecodeError:
|
|
364
|
+
error_body = {}
|
|
365
|
+
|
|
366
|
+
if error_body and error_body.get("message", ""):
|
|
367
|
+
message += f": {error_body['message']}"
|
|
368
|
+
|
|
369
|
+
return self._request_error(message=message, error=e)
|
|
370
|
+
|
|
371
|
+
except httpx.TimeoutException as e:
|
|
372
|
+
# Server timed out
|
|
373
|
+
logger.error(msg=str(e))
|
|
374
|
+
|
|
375
|
+
message = f"{method} request to {endpoint} timed out"
|
|
376
|
+
return self._request_error(message=message, error=e)
|
|
377
|
+
|
|
378
|
+
except httpx.RequestError as e:
|
|
379
|
+
# Some other problem with the request
|
|
380
|
+
logger.error(msg=str(e))
|
|
381
|
+
|
|
382
|
+
message = f"{method} request to {endpoint} failed"
|
|
383
|
+
return self._request_error(message=message, error=e)
|
|
384
|
+
|
|
385
|
+
except json.decoder.JSONDecodeError as e:
|
|
386
|
+
# Server returned something that isn't JSON
|
|
387
|
+
logger.error(msg=str(e))
|
|
388
|
+
|
|
389
|
+
message = f"{method} request to {endpoint} did not return parseable JSON"
|
|
390
|
+
return self._request_error(message=message, error=e)
|
|
391
|
+
|
|
392
|
+
async def aclose(self):
|
|
393
|
+
await self._client.aclose()
|
|
394
|
+
|
|
395
|
+
async def close(self):
|
|
396
|
+
await self.aclose()
|
|
397
|
+
|
|
398
|
+
async def __aenter__(self):
|
|
399
|
+
return self
|
|
400
|
+
|
|
401
|
+
async def __aexit__(self, *args):
|
|
402
|
+
await self.aclose()
|
|
403
|
+
|
|
404
|
+
async def log_in(self, username_or_email: str, password: str) -> bool:
|
|
405
|
+
payload = {"username": username_or_email, "password": password}
|
|
406
|
+
method = "POST"
|
|
407
|
+
|
|
408
|
+
resp = await self.request(method=method, endpoint="user/login", data=payload)
|
|
409
|
+
|
|
410
|
+
if resp is None:
|
|
411
|
+
# Problem with the request that was suppressed, just return False
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
self.jwt_token = resp["jwt"]
|
|
415
|
+
self.headers["Authorization"] = "Bearer " + self.jwt_token
|
|
416
|
+
self.current_username_or_email = username_or_email
|
|
417
|
+
self.current_password = password
|
|
418
|
+
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
async def is_logged_in(self) -> bool:
|
|
422
|
+
return bool(self.jwt_token)
|
|
423
|
+
|
|
424
|
+
async def log_out(self) -> bool:
|
|
425
|
+
method = "POST"
|
|
426
|
+
|
|
427
|
+
resp = await self.request(method=method, endpoint="user/logout")
|
|
428
|
+
|
|
429
|
+
if resp is None:
|
|
430
|
+
# Problem with the request that was suppressed, just return False
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
self.jwt_token = ""
|
|
434
|
+
self.current_username_or_email = ""
|
|
435
|
+
self.current_password = ""
|
|
436
|
+
|
|
437
|
+
return True
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
async def nodeinfo(self) -> dict:
|
|
441
|
+
"""Dict containing the instance's nodeinfo output.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
dict: deserialized copy of the instance's nodeinfo or None if the request failed and exception ignored
|
|
445
|
+
"""
|
|
446
|
+
if self._nodeinfo:
|
|
447
|
+
return self._nodeinfo
|
|
448
|
+
|
|
449
|
+
method = "GET"
|
|
450
|
+
endpoint = self.instance_url + "/nodeinfo/2.1.json"
|
|
451
|
+
|
|
452
|
+
resp = await self.request(method=method, endpoint=endpoint)
|
|
453
|
+
self._nodeinfo = resp
|
|
454
|
+
|
|
455
|
+
return self._nodeinfo if self._nodeinfo else None
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
async def instance_version(self) -> semver.Version:
|
|
459
|
+
"""The instance's software version from the nodeinfo.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
semver.Version: software version of the piefed instance
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
current_nodeinfo = await self.nodeinfo
|
|
466
|
+
|
|
467
|
+
if current_nodeinfo is None:
|
|
468
|
+
# Problem with the request that was suppressed, just return False
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
version_str = current_nodeinfo["software"]["version"]
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
self._instance_version = semver.Version.parse(version_str)
|
|
475
|
+
return self._instance_version
|
|
476
|
+
except ValueError as e:
|
|
477
|
+
logger.error(msg=str(e))
|
|
478
|
+
logger.error(f"_instance_version={version_str}")
|
|
479
|
+
|
|
480
|
+
message = f"Instance version from nodeinfo not parseable: {version_str}"
|
|
481
|
+
return self._request_error(message=message, error=e)
|
pieforhead/exceptions.py
ADDED
pieforhead/image.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from io import IOBase
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import BinaryIO
|
|
5
|
+
|
|
6
|
+
from .clients import AsyncPieFedClient, PieFedClient
|
|
7
|
+
from .exceptions import ImageException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Image:
|
|
11
|
+
"""Class to hold image endpoints."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: PieFedClient):
|
|
14
|
+
self._client = client
|
|
15
|
+
|
|
16
|
+
def upload(
|
|
17
|
+
self,
|
|
18
|
+
image: str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str],
|
|
19
|
+
other_endpoint: str | None = None,
|
|
20
|
+
) -> dict | None:
|
|
21
|
+
"""General function for uploading an image.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
image (str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str]): image to upload. Tuple argument
|
|
25
|
+
must have form suitable for httpx.request, meaning that it is of the form:
|
|
26
|
+
(filename, image_bytes, mime_type).
|
|
27
|
+
other_endpoint (str | None, optional): Used to divert request to one of the other upload endpoints.
|
|
28
|
+
Defaults to None.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ImageException: raised if image type isn't understood or if unknown endpoint is provided
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict | None: returns dict if successful and None if there is an error and exceptions were suppressed by the
|
|
35
|
+
client.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
opened_file = False
|
|
39
|
+
file_obj = None
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Figure out how to handle the image depending on the type provided
|
|
43
|
+
if isinstance(image, str | PathLike):
|
|
44
|
+
path = Path(image)
|
|
45
|
+
opened_file = True
|
|
46
|
+
file_obj = path.open("rb")
|
|
47
|
+
elif isinstance(image, tuple):
|
|
48
|
+
file_obj = image
|
|
49
|
+
elif isinstance(image, IOBase):
|
|
50
|
+
file_obj = image
|
|
51
|
+
file_obj.seek(0)
|
|
52
|
+
else:
|
|
53
|
+
if self._client.raise_exceptions:
|
|
54
|
+
raise ImageException(f"Incompatible image type provided for upload: {type(image)}")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if not other_endpoint:
|
|
58
|
+
endpoint = "upload/image"
|
|
59
|
+
elif other_endpoint == "community":
|
|
60
|
+
endpoint = "upload/community_image"
|
|
61
|
+
elif other_endpoint == "user":
|
|
62
|
+
endpoint = "upload/user_image"
|
|
63
|
+
else:
|
|
64
|
+
if self._client.raise_exceptions:
|
|
65
|
+
raise ImageException(f"Unknown image endpoint provided: {other_endpoint}")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
files = {"file": file_obj}
|
|
69
|
+
return self._client.request(method="POST", endpoint=endpoint, files=files)
|
|
70
|
+
finally:
|
|
71
|
+
# Make sure to still close the file object no matter what happened with the request
|
|
72
|
+
if opened_file:
|
|
73
|
+
file_obj.close()
|
|
74
|
+
|
|
75
|
+
def upload_community_image(
|
|
76
|
+
self,
|
|
77
|
+
image: str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str],
|
|
78
|
+
) -> dict | None:
|
|
79
|
+
return self.upload(image=image, other_endpoint="community")
|
|
80
|
+
|
|
81
|
+
def upload_user_image(
|
|
82
|
+
self,
|
|
83
|
+
image: str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str],
|
|
84
|
+
) -> dict | None:
|
|
85
|
+
return self.upload(image=image, other_endpoint="user")
|
|
86
|
+
|
|
87
|
+
def delete(self, url: str) -> dict | None:
|
|
88
|
+
"""Delete an uploaded image by providing its url.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
url (str): url of the uploaded image to delete
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict | None: should be {"result": "ok"} if successful. None if there is an exception that is suppressed
|
|
95
|
+
"""
|
|
96
|
+
return self._client.request(method="POST", endpoint="image/delete", data={"file": url})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ImageAsync:
|
|
100
|
+
"""Class to hold async image endpoints."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, client: AsyncPieFedClient):
|
|
103
|
+
self._client = client
|
|
104
|
+
|
|
105
|
+
async def upload(
|
|
106
|
+
self,
|
|
107
|
+
image: str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str],
|
|
108
|
+
other_endpoint: str | None = None,
|
|
109
|
+
) -> dict | None:
|
|
110
|
+
"""General function for uploading an image.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
image (str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str]): image to upload. Tuple argument
|
|
114
|
+
must have form suitable for httpx.request, meaning that it is of the form:
|
|
115
|
+
(filename, image_bytes, mime_type).
|
|
116
|
+
other_endpoint (str | None, optional): Used to divert request to one of the other upload endpoints.
|
|
117
|
+
Defaults to None.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ImageException: raised if image type isn't understood or if unknown endpoint is provided
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
dict | None: returns dict if successful and None if there is an error and exceptions were suppressed by the
|
|
124
|
+
client.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
opened_file = False
|
|
128
|
+
file_obj = None
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
# Figure out how to handle the image depending on the type provided
|
|
132
|
+
if isinstance(image, str | PathLike):
|
|
133
|
+
path = Path(image)
|
|
134
|
+
opened_file = True
|
|
135
|
+
file_obj = path.open("rb")
|
|
136
|
+
elif isinstance(image, tuple):
|
|
137
|
+
file_obj = image
|
|
138
|
+
elif isinstance(image, IOBase):
|
|
139
|
+
file_obj = image
|
|
140
|
+
file_obj.seek(0)
|
|
141
|
+
else:
|
|
142
|
+
if self._client.raise_exceptions:
|
|
143
|
+
raise ImageException(f"Incompatible image type provided for upload: {type(image)}")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if not other_endpoint:
|
|
147
|
+
endpoint = "upload/image"
|
|
148
|
+
elif other_endpoint == "community":
|
|
149
|
+
endpoint = "upload/community_image"
|
|
150
|
+
elif other_endpoint == "user":
|
|
151
|
+
endpoint = "upload/user_image"
|
|
152
|
+
else:
|
|
153
|
+
if self._client.raise_exceptions:
|
|
154
|
+
raise ImageException(f"Unknown image endpoint provided: {other_endpoint}")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
files = {"file": file_obj}
|
|
158
|
+
return await self._client.request(method="POST", endpoint=endpoint, files=files)
|
|
159
|
+
finally:
|
|
160
|
+
# Make sure to still close the file object no matter what happened with the request
|
|
161
|
+
if opened_file:
|
|
162
|
+
file_obj.close()
|
|
163
|
+
|
|
164
|
+
async def upload_community_image(
|
|
165
|
+
self,
|
|
166
|
+
image: str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str],
|
|
167
|
+
) -> dict | None:
|
|
168
|
+
return await self.upload(image=image, other_endpoint="community")
|
|
169
|
+
|
|
170
|
+
async def upload_user_image(
|
|
171
|
+
self,
|
|
172
|
+
image: str | BinaryIO | PathLike | tuple[str, bytes | bytearray, str],
|
|
173
|
+
) -> dict | None:
|
|
174
|
+
return await self.upload(image=image, other_endpoint="user")
|
|
175
|
+
|
|
176
|
+
async def delete(self, url: str) -> dict | None:
|
|
177
|
+
"""Delete an uploaded image by providing its url.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
url (str): url of the uploaded image to delete
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
dict | None: should be {"result": "ok"} if successful. None if there is an exception that is suppressed
|
|
184
|
+
"""
|
|
185
|
+
return await self._client.request(method="POST", endpoint="image/delete", data={"file": url})
|
pieforhead/pieforhead.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import semver
|
|
4
|
+
|
|
5
|
+
from .clients import AsyncPieFedClient, PieFedClient
|
|
6
|
+
from .site import Site, SiteAsync
|
|
7
|
+
from .image import Image, ImageAsync
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PieFed:
|
|
13
|
+
_client: PieFedClient
|
|
14
|
+
instance_url: str
|
|
15
|
+
|
|
16
|
+
def __init__(self, instance_url: str, timeout: float = 5.0, raise_exceptions: bool = True):
|
|
17
|
+
self.instance_url = instance_url.removesuffix("/")
|
|
18
|
+
self._client = PieFedClient(instance_url=instance_url, timeout=timeout, raise_exceptions=raise_exceptions)
|
|
19
|
+
|
|
20
|
+
# Initialize attributes used for different endpoint groupings
|
|
21
|
+
self.site = Site(client=self._client)
|
|
22
|
+
self.image = Image(client=self._client)
|
|
23
|
+
|
|
24
|
+
def __enter__(self):
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def log_in(self, username_or_email: str, password: str) -> bool:
|
|
28
|
+
return self._client.log_in(username_or_email, password)
|
|
29
|
+
|
|
30
|
+
def is_logged_in(self) -> bool:
|
|
31
|
+
return self._client.is_logged_in()
|
|
32
|
+
|
|
33
|
+
def log_out(self) -> bool:
|
|
34
|
+
return self._client.log_out()
|
|
35
|
+
|
|
36
|
+
def close(self):
|
|
37
|
+
self._client.close()
|
|
38
|
+
|
|
39
|
+
def __exit__(self, *args):
|
|
40
|
+
self.close()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def instance_version(self) -> semver.Version:
|
|
44
|
+
return self._client.instance_version
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PieFedAsync:
|
|
48
|
+
_client: AsyncPieFedClient
|
|
49
|
+
instance_url: str
|
|
50
|
+
|
|
51
|
+
def __init__(self, instance_url: str, timeout: float = 5.0, raise_exceptions: bool = True):
|
|
52
|
+
self.instance_url = instance_url.removesuffix("/")
|
|
53
|
+
self._client = AsyncPieFedClient(instance_url=instance_url, timeout=timeout, raise_exceptions=raise_exceptions)
|
|
54
|
+
|
|
55
|
+
# Initialize attributes used for different endpoint groupings
|
|
56
|
+
self.site = SiteAsync(client=self._client)
|
|
57
|
+
self.image = ImageAsync(client=self._client)
|
|
58
|
+
|
|
59
|
+
async def __aenter__(self):
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
async def log_in(self, username_or_email: str, password: str) -> bool:
|
|
63
|
+
return await self._client.log_in(username_or_email, password)
|
|
64
|
+
|
|
65
|
+
async def is_logged_in(self) -> bool:
|
|
66
|
+
return await self._client.is_logged_in()
|
|
67
|
+
|
|
68
|
+
async def log_out(self) -> bool:
|
|
69
|
+
return await self._client.log_out()
|
|
70
|
+
|
|
71
|
+
async def close(self):
|
|
72
|
+
await self._client.close()
|
|
73
|
+
|
|
74
|
+
async def __aexit__(self, *args):
|
|
75
|
+
await self.close()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
async def instance_version(self) -> semver.Version:
|
|
79
|
+
return await self._client.instance_version
|
pieforhead/site.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import semver
|
|
2
|
+
|
|
3
|
+
from .clients import PieFedClient, AsyncPieFedClient
|
|
4
|
+
from .exceptions import NotImplementedError, SiteException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Site:
|
|
8
|
+
"""Class to hold Site endpoints."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client: PieFedClient):
|
|
11
|
+
self._client = client
|
|
12
|
+
|
|
13
|
+
def get(self) -> dict | None:
|
|
14
|
+
"""Dumps value returned from site endpoint into a dict.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
dict: contents of GET site endpoint
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
return self._client.request(method="GET", endpoint="site")
|
|
21
|
+
|
|
22
|
+
def version(self) -> semver.Version | None:
|
|
23
|
+
"""Fetch software version from site/version endpoint.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
semver.Version: software version
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
resp = self._client.request(method="GET", endpoint="site/version")
|
|
30
|
+
|
|
31
|
+
if resp is None:
|
|
32
|
+
# Some problem with the request, but exceptions are suppressed
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if "version" in resp:
|
|
36
|
+
try:
|
|
37
|
+
return semver.Version.parse(resp["version"])
|
|
38
|
+
except ValueError as e:
|
|
39
|
+
raise SiteException(f"Version not parseable: {resp['version']}") from e
|
|
40
|
+
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
def block(self, block: bool, instance_id: int) -> bool | None:
|
|
44
|
+
"""Block or unblock an instance with a given id.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
block (bool): True = block, False = unblock
|
|
48
|
+
instance_id (int): id of instance
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
bool: True if instance is now blocked, False if it is now unblocked
|
|
52
|
+
"""
|
|
53
|
+
payload = {"block": block, "instance_id": instance_id}
|
|
54
|
+
|
|
55
|
+
resp = self._client.request(method="POST", endpoint="site/block", data=payload)
|
|
56
|
+
|
|
57
|
+
if resp is None:
|
|
58
|
+
# Some problem with the request, but exceptions are suppressed
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
return resp["blocked"]
|
|
62
|
+
|
|
63
|
+
def instance_chooser(self):
|
|
64
|
+
if self._client.raise_exceptions:
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
else:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
def instance_chooser_search(self):
|
|
70
|
+
if self._client.raise_exceptions:
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
else:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SiteAsync:
|
|
77
|
+
"""Class to hold async Site endpoints."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, client: AsyncPieFedClient):
|
|
80
|
+
self._client = client
|
|
81
|
+
|
|
82
|
+
async def get(self) -> dict | None:
|
|
83
|
+
"""Dumps value returned from site endpoint into a dict.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
dict: contents of GET site endpoint
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
return await self._client.request(method="GET", endpoint="site")
|
|
90
|
+
|
|
91
|
+
async def version(self) -> semver.Version | None:
|
|
92
|
+
"""Fetch software version from site/version endpoint.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
semver.Version: software version
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
resp = await self._client.request(method="GET", endpoint="site/version")
|
|
99
|
+
|
|
100
|
+
if resp is None:
|
|
101
|
+
# Some problem with the request, but exceptions are suppressed
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if "version" in resp:
|
|
105
|
+
try:
|
|
106
|
+
return semver.Version.parse(resp["version"])
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
raise SiteException(f"Version not parseable: {resp['version']}") from e
|
|
109
|
+
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
async def block(self, block: bool, instance_id: int) -> bool | None:
|
|
113
|
+
"""Block or unblock an instance with a given id.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
block (bool): True = block, False = unblock
|
|
117
|
+
instance_id (int): id of instance
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
bool: True if instance is now blocked, False if it is now unblocked
|
|
121
|
+
"""
|
|
122
|
+
payload = {"block": block, "instance_id": instance_id}
|
|
123
|
+
|
|
124
|
+
resp = await self._client.request(method="POST", endpoint="site/block", data=payload)
|
|
125
|
+
|
|
126
|
+
if resp is None:
|
|
127
|
+
# Some problem with the request, but exceptions are suppressed
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
return resp["blocked"]
|
|
131
|
+
|
|
132
|
+
async def instance_chooser(self):
|
|
133
|
+
if self._client.raise_exceptions:
|
|
134
|
+
raise NotImplementedError
|
|
135
|
+
else:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
async def instance_chooser_search(self):
|
|
139
|
+
if self._client.raise_exceptions:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
else:
|
|
142
|
+
return
|
pieforhead/utils.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pieforhead
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A python library for interacting with the PieFed API
|
|
5
|
+
Project-URL: Homepage, https://github.com/wjs018/pieforhead
|
|
6
|
+
Project-URL: Issues, https://github.com/wjs018/pieforhead/issues
|
|
7
|
+
Author-email: wjs018 <wjs018@gmail.com>
|
|
8
|
+
License-Expression: Unlicense
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: fediverse,piefed
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: httpx
|
|
16
|
+
Requires-Dist: semver
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# pieforhead
|
|
20
|
+
|
|
21
|
+
A python wrapper for the PieFed API supporting both synchronous and asynchronous usage. This is an early work in progress, and few endpoints are supported yet.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Usage Notes
|
|
26
|
+
|
|
27
|
+
In general, all successful requests to the server should return a `dict` with the server's json response. Due to the alpha nature of the PieFed API, the maintenance burden of creating and updating response models as features/fields are added to the API is a burden that I don't want to take on at this time. Please refer to the official PieFed OpenAPI specification ([stable version](https://stable.wjs018.xyz/api/alpha/swagger), [development version](https://crust.piefed.social/api/alpha/swagger)) to see what your code should expect as a response.
|
|
28
|
+
|
|
29
|
+
By default, exceptions will be raised if something goes wrong (e.g. malformed request, network timeout, 5XX errors, etc.) to provide a trace. However, to suppress these exceptions, you can pass the `raise_exceptions=False` keyword argument when instantiating your client. This will cause a returned value of `None` instead of raising an exception. Please make sure that your code handles such a case appropriately.
|
|
30
|
+
|
|
31
|
+
## Synchronous Client Usage
|
|
32
|
+
|
|
33
|
+
Users familiar with network libraries like `requests` would probably be most familiar with synchronous usage. However, to make network requests a bit more efficient, this library makes use of `Client`s within `httpx` where you can take advantage of connection pooling for multiple network requests. This means that when you instantiate a `PieFed` class, it is best practice to close it when you are done with it. Here are a couple examples that show how this can be done.
|
|
34
|
+
|
|
35
|
+
These snippets all do the same three things:
|
|
36
|
+
|
|
37
|
+
1. Create a `PieFed` object with the given instance url (this creates the client)
|
|
38
|
+
2. Log in using a username and password
|
|
39
|
+
3. Close the client after we no longer need it
|
|
40
|
+
|
|
41
|
+
Closing it manually using the `close` function:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from piefedhead import PieFed
|
|
45
|
+
|
|
46
|
+
piefed = PieFed("https://instance.tld")
|
|
47
|
+
piefed.log_in("username", "password")
|
|
48
|
+
|
|
49
|
+
piefed.close()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Using a context manager (a `with` block):
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Inside a context manager
|
|
56
|
+
with PieFed("https://instance.tld") as piefed:
|
|
57
|
+
piefed.log_in("username", "password")
|
|
58
|
+
|
|
59
|
+
# The connection is closed after leaving the `with` block
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Using a callback that ensures the `close` function is called when execution has ended:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# Registering an event handler
|
|
66
|
+
import atexit
|
|
67
|
+
|
|
68
|
+
piefed = PieFed("https://instance.tld")
|
|
69
|
+
atexit.register(piefed.close) # Automatically closes the client at program termination
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Asynchronous Client Usage
|
|
73
|
+
|
|
74
|
+
To use this libray in an async manner, there is a separate client, `PieFedAsync` that should be used. Everything else should work exactly the same as the synchronous client, except that results will need to be `await`ed.
|
|
75
|
+
|
|
76
|
+
Here is a snippet that does the same three actions as outlined above for the synchronous client, but using the `PieFedAsync` client.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Put things in an async function so that we can use the await keyword
|
|
80
|
+
async def main():
|
|
81
|
+
piefed_async = PieFedAsync("https://instance.tld")
|
|
82
|
+
await piefed_async.log_in("username", "password") # Notice the await keyword
|
|
83
|
+
|
|
84
|
+
await piefed_async.close()
|
|
85
|
+
|
|
86
|
+
# Start the event loop and execute the async function
|
|
87
|
+
asyncio.run(main())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Similar to the synchronous client, you can also use a context handler to automatically close the async client:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
async def main():
|
|
94
|
+
async with PieFedAsync("https://instance.tld") as piefed_async:
|
|
95
|
+
await piefed_async.log_in("username", "password")
|
|
96
|
+
|
|
97
|
+
# Client is closed automatically once outside the context handler
|
|
98
|
+
|
|
99
|
+
# Start the event loop and execute the async function
|
|
100
|
+
asyncio.run(main())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Endpoint Coverage
|
|
104
|
+
|
|
105
|
+
This project is in an early stage. Here is a high level summary of where things stand with implementing the wrapper around different PieFed API endpoint groups:
|
|
106
|
+
|
|
107
|
+
| Endpoint Group | Sync Client | Async Client | Notes |
|
|
108
|
+
| :-------------: | :---------: | :----------: | :--------------------------------------- |
|
|
109
|
+
| Site | ✅ | ✅ | Instance chooser endpoints unimplemented |
|
|
110
|
+
| Upload | ✅ | ✅ | |
|
|
111
|
+
| User | 🟡 | 🟡 | Auth endpoints implemented |
|
|
112
|
+
| Post | ❌ | ❌ | |
|
|
113
|
+
| Comment | ❌ | ❌ | |
|
|
114
|
+
| Community | ❌ | ❌ | |
|
|
115
|
+
| Feed | ❌ | ❌ | |
|
|
116
|
+
| Topic | ❌ | ❌ | |
|
|
117
|
+
| Private Message | ❌ | ❌ | |
|
|
118
|
+
| Admin | ❌ | ❌ | |
|
|
119
|
+
| Misc. | ❌ | ❌ | |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pieforhead/__init__.py,sha256=nvEcwCSSK6K4ywr78d9EXgk7WJCcGcaK86Zjj9uNSxc,134
|
|
2
|
+
pieforhead/clients.py,sha256=RBWIfrR3dhlQI2nwXgxQ9VOeRhOj-B1pjoxSKc6jqEs,16167
|
|
3
|
+
pieforhead/exceptions.py,sha256=Q-JiNqUJ7Bf9sThXfXqCfI9zs7UXEO2af4P9SbD5EG8,185
|
|
4
|
+
pieforhead/image.py,sha256=0hiv6FCmd8-tICWb8sWZzyIGcBTIN25lzPPtEc8FXKM,6984
|
|
5
|
+
pieforhead/pieforhead.py,sha256=U9sJExCd5hDf9m2BxBMg1XaE_yinDdUDcpUFhS82EiM,2386
|
|
6
|
+
pieforhead/site.py,sha256=vgWNHg2Ey2sJRglTiGQifeeOHMyH121wBV7bskq14Jc,4115
|
|
7
|
+
pieforhead/utils.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
|
|
8
|
+
pieforhead-0.0.1.dist-info/METADATA,sha256=rJp0cdOxcGaiOLFWhuKSUy8JptPh1mlHsk7yxd0bbUM,5772
|
|
9
|
+
pieforhead-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
pieforhead-0.0.1.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
11
|
+
pieforhead-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|