binaaz-python-sdk 0.1.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.
- binaaz_python_sdk-0.1.0.dist-info/METADATA +205 -0
- binaaz_python_sdk-0.1.0.dist-info/RECORD +17 -0
- binaaz_python_sdk-0.1.0.dist-info/WHEEL +4 -0
- binaaz_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- binaaz_sdk/__init__.py +97 -0
- binaaz_sdk/_core.py +418 -0
- binaaz_sdk/_version.py +1 -0
- binaaz_sdk/client.py +50 -0
- binaaz_sdk/config.py +83 -0
- binaaz_sdk/enums.py +73 -0
- binaaz_sdk/errors.py +107 -0
- binaaz_sdk/messages.py +60 -0
- binaaz_sdk/messages.yaml +62 -0
- binaaz_sdk/models.py +365 -0
- binaaz_sdk/py.typed +0 -0
- binaaz_sdk/queries.py +54 -0
- binaaz_sdk/resources.py +450 -0
binaaz_sdk/resources.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import IO, Any, Mapping, Optional, Union
|
|
8
|
+
|
|
9
|
+
from . import queries
|
|
10
|
+
from ._core import CodeReply, Flow, Op
|
|
11
|
+
from .enums import PHOTO_ANGLES, PHOTOS_MAX, PHOTOS_MIN, ItemTab
|
|
12
|
+
from .errors import (
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
BadRequestError,
|
|
15
|
+
CodeRejectedError,
|
|
16
|
+
InvalidRequestError,
|
|
17
|
+
ItemError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
NotSupportedError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
)
|
|
23
|
+
from .messages import text
|
|
24
|
+
from .models import (
|
|
25
|
+
CategoryNode,
|
|
26
|
+
CodeSent,
|
|
27
|
+
Item,
|
|
28
|
+
ItemPage,
|
|
29
|
+
ItemResult,
|
|
30
|
+
Listing,
|
|
31
|
+
PaidService,
|
|
32
|
+
Payment,
|
|
33
|
+
Photo,
|
|
34
|
+
Place,
|
|
35
|
+
Profile,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
OPERATORS = ("10", "50", "51", "55", "60", "70", "77", "99")
|
|
39
|
+
PAGE_MAX = 100
|
|
40
|
+
|
|
41
|
+
ListingLike = Union[Listing, Mapping[str, Any]]
|
|
42
|
+
PhotoLike = Union[bytes, bytearray, str, "os.PathLike[str]", IO[bytes]]
|
|
43
|
+
TabLike = Union[ItemTab, str, None]
|
|
44
|
+
|
|
45
|
+
CREATE_STATUS = {
|
|
46
|
+
"RESTRICTED": "error.status_restricted",
|
|
47
|
+
"UNVERIFIED": "error.status_unverified",
|
|
48
|
+
"INVALID": "error.status_invalid",
|
|
49
|
+
"FAILURE": "error.status_failure",
|
|
50
|
+
}
|
|
51
|
+
UPDATE_STATUS = {
|
|
52
|
+
"LOCKED": "error.status_locked",
|
|
53
|
+
"FORBIDDEN": "error.status_forbidden",
|
|
54
|
+
"NOT_FOUND": "error.status_not_found",
|
|
55
|
+
"INVALID": "error.status_invalid",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def required(value: Any, name: str, limit: int = 255) -> str:
|
|
60
|
+
result = str(value if value is not None else "").strip()
|
|
61
|
+
if not result:
|
|
62
|
+
raise InvalidRequestError(text("value.empty", field=name))
|
|
63
|
+
if len(result) > limit:
|
|
64
|
+
raise InvalidRequestError(text("value.too_long", field=name, limit=limit))
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def site_id(value: Any, name: str) -> str:
|
|
69
|
+
result = required(value, name, 64)
|
|
70
|
+
if any(ch in result for ch in "/?#& "):
|
|
71
|
+
raise InvalidRequestError(text("value.id", field=name, value=value))
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def whole(value: Any, name: str, low: int, high: int) -> int:
|
|
76
|
+
if isinstance(value, bool) or not isinstance(value, int) or not low <= value <= high:
|
|
77
|
+
raise InvalidRequestError(text("value.range", field=name, low=low, high=high, value=value))
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def local_phone(raw: Any) -> str:
|
|
82
|
+
digits = "".join(ch for ch in str(raw or "") if ch.isdigit())
|
|
83
|
+
if digits.startswith("994"):
|
|
84
|
+
digits = digits[3:]
|
|
85
|
+
if digits.startswith("0"):
|
|
86
|
+
digits = digits[1:]
|
|
87
|
+
if len(digits) != 9 or not digits.startswith(OPERATORS):
|
|
88
|
+
raise InvalidRequestError(text("value.phone", value=raw))
|
|
89
|
+
return "0" + digits
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def listing(value: ListingLike) -> Listing:
|
|
93
|
+
if isinstance(value, Listing):
|
|
94
|
+
return value
|
|
95
|
+
try:
|
|
96
|
+
return Listing.model_validate(dict(value))
|
|
97
|
+
except ValueError as exc:
|
|
98
|
+
raise InvalidRequestError(str(exc)) from None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def item_tab(value: TabLike) -> Optional[str]:
|
|
102
|
+
if value is None or value == "":
|
|
103
|
+
return None
|
|
104
|
+
try:
|
|
105
|
+
return ItemTab(str(value).upper()).value
|
|
106
|
+
except ValueError:
|
|
107
|
+
named = {t.name: t.value for t in ItemTab}
|
|
108
|
+
if str(value).upper() in named:
|
|
109
|
+
return named[str(value).upper()]
|
|
110
|
+
raise InvalidRequestError(text("value.choice", field="tab", choices=tuple(named), value=value)) from None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _detail(errors: list[Any]) -> str:
|
|
114
|
+
parts = []
|
|
115
|
+
for error in errors:
|
|
116
|
+
if isinstance(error, Mapping):
|
|
117
|
+
where = ".".join(str(p) for p in error.get("path") or []) or "item"
|
|
118
|
+
parts.append(f"{where}: {error.get('message')}")
|
|
119
|
+
return "; ".join(parts)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def unwrap(data: Mapping[str, Any], name: str, statuses: Optional[Mapping[str, str]] = None) -> Mapping[str, Any]:
|
|
123
|
+
payload = data.get(name)
|
|
124
|
+
if not isinstance(payload, Mapping):
|
|
125
|
+
raise AuthenticationError(text("error.session"))
|
|
126
|
+
errors = [e for e in payload.get("errors") or [] if isinstance(e, Mapping)]
|
|
127
|
+
status = payload.get("status")
|
|
128
|
+
detail = _detail(errors)
|
|
129
|
+
if statuses and status in statuses:
|
|
130
|
+
until = payload.get("lockedUntil")
|
|
131
|
+
params = {"until": f" until {until}" if until else ""} if status == "LOCKED" else {}
|
|
132
|
+
message = text(statuses[str(status)], **params) + (f": {detail}" if detail else "")
|
|
133
|
+
kind = NotFoundError if status == "NOT_FOUND" else ItemError
|
|
134
|
+
if kind is ItemError:
|
|
135
|
+
raise ItemError(message, status=str(status), errors=errors, locked_until=until, body=payload)
|
|
136
|
+
raise NotFoundError(message, body=payload)
|
|
137
|
+
if errors:
|
|
138
|
+
raise ItemError(text("error.validation", detail=detail), errors=errors, body=payload)
|
|
139
|
+
return payload
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def read_photo(photo: PhotoLike, filename: Optional[str], content_type: Optional[str]) -> tuple[str, bytes, str]:
|
|
143
|
+
if isinstance(photo, (bytes, bytearray)):
|
|
144
|
+
content, name = bytes(photo), filename or "photo.jpg"
|
|
145
|
+
elif isinstance(photo, (str, os.PathLike)):
|
|
146
|
+
path = Path(photo)
|
|
147
|
+
content, name = path.read_bytes(), filename or path.name
|
|
148
|
+
elif hasattr(photo, "read"):
|
|
149
|
+
content = photo.read()
|
|
150
|
+
if not isinstance(content, (bytes, bytearray)):
|
|
151
|
+
raise InvalidRequestError(text("value.photo_file", value=photo))
|
|
152
|
+
content = bytes(content)
|
|
153
|
+
name = filename or os.path.basename(str(getattr(photo, "name", "") or "")) or "photo.jpg"
|
|
154
|
+
else:
|
|
155
|
+
raise InvalidRequestError(text("value.photo_file", value=photo))
|
|
156
|
+
if not content:
|
|
157
|
+
raise InvalidRequestError(text("value.empty", field="photo"))
|
|
158
|
+
kind = content_type or mimetypes.guess_type(name)[0] or "image/jpeg"
|
|
159
|
+
return name, content, kind
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _graphql(query: str, parse: Any, variables: Optional[Mapping[str, Any]] = None, *, name: str = "",
|
|
163
|
+
safe: bool = False, session: bool = True, action: str = "The request") -> Op:
|
|
164
|
+
body: dict[str, Any] = {"query": query, "variables": dict(variables or {})}
|
|
165
|
+
if name:
|
|
166
|
+
body["operationName"] = name
|
|
167
|
+
return Op("POST", "base_url", "graphql", parse, body=body, safe=safe, session=session, action=action)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _Resource:
|
|
171
|
+
def __init__(self, client: Any) -> None:
|
|
172
|
+
self._client = client
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def _settings(self) -> Any:
|
|
176
|
+
return self._client.settings
|
|
177
|
+
|
|
178
|
+
def _run(self, op: Op) -> Any:
|
|
179
|
+
return self._client._run(op)
|
|
180
|
+
|
|
181
|
+
def _flow(self, flow: Flow[Any]) -> Any:
|
|
182
|
+
return self._client._flow(flow)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _profile(data: Mapping[str, Any]) -> Profile:
|
|
186
|
+
body = data.get("profile")
|
|
187
|
+
if not isinstance(body, Mapping) or not body:
|
|
188
|
+
raise AuthenticationError(text("error.no_profile"))
|
|
189
|
+
return Profile.from_api(body)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _company(data: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
193
|
+
body = data.get("profile")
|
|
194
|
+
if not isinstance(body, Mapping) or not body:
|
|
195
|
+
raise AuthenticationError(text("error.no_profile"))
|
|
196
|
+
if body.get("__typename") != "CompanyProfile":
|
|
197
|
+
raise NotSupportedError(text("error.not_company", kind=body.get("__typename")))
|
|
198
|
+
return body
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _sign_in_payload(data: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
202
|
+
payload = data.get("signIn")
|
|
203
|
+
payload = payload if isinstance(payload, Mapping) else {}
|
|
204
|
+
errors = [e for e in payload.get("errors") or [] if isinstance(e, Mapping)]
|
|
205
|
+
if errors or not payload.get("profile"):
|
|
206
|
+
raise BadRequestError(text("error.login", detail=_detail(errors) or "no profile"), body=payload)
|
|
207
|
+
return payload
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _code_error(reply: CodeReply, retried_message: bool = False) -> Exception:
|
|
211
|
+
kind = str(reply.body.get("error") or "")
|
|
212
|
+
detail = str(reply.body.get("message") or kind)
|
|
213
|
+
if kind in ("code_requests", "failed_attempts"):
|
|
214
|
+
return RateLimitError(text("error.code_limit", detail=detail), status_code=reply.status, body=reply.body)
|
|
215
|
+
if kind == "phone":
|
|
216
|
+
return BadRequestError(text("error.code_phone", detail=detail), status_code=reply.status, body=reply.body)
|
|
217
|
+
key = "error.code_reused" if retried_message and reply.attempts > 1 else "error.code_rejected"
|
|
218
|
+
return CodeRejectedError(text(key, detail=detail), status_code=reply.status, body=reply.body)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class Auth(_Resource):
|
|
222
|
+
def profile(self) -> Any:
|
|
223
|
+
return self._run(_graphql(queries.PROFILE, _profile, name="Profile", safe=True))
|
|
224
|
+
|
|
225
|
+
def login(self, email: str, password: str) -> Any:
|
|
226
|
+
variables = {"input": {"emailAndPassword": {"email": required(email, "email"),
|
|
227
|
+
"password": required(password, "password", 200)}}}
|
|
228
|
+
return self._flow(self._sign_in(variables))
|
|
229
|
+
|
|
230
|
+
def send_code(self, phone: str) -> Any:
|
|
231
|
+
number = local_phone(phone)
|
|
232
|
+
|
|
233
|
+
def parse(reply: CodeReply) -> CodeSent:
|
|
234
|
+
if reply.status >= 400:
|
|
235
|
+
raise _code_error(reply)
|
|
236
|
+
left = reply.body.get("code_requests_left")
|
|
237
|
+
return CodeSent(number, left if isinstance(left, int) and not isinstance(left, bool) else None,
|
|
238
|
+
reply.attempts > 1, dict(reply.body))
|
|
239
|
+
|
|
240
|
+
return self._run(Op("POST", "login_url", "code_send", parse, body={"phone": number}, flaky=True,
|
|
241
|
+
session=False, action="Sending the SMS code"))
|
|
242
|
+
|
|
243
|
+
def verify_code(self, phone: str, code: str) -> Any:
|
|
244
|
+
return self._flow(self._verify(local_phone(phone), required(code, "code", 12)))
|
|
245
|
+
|
|
246
|
+
def _verify(self, number: str, code: str) -> Flow[Profile]:
|
|
247
|
+
def parse(reply: CodeReply) -> str:
|
|
248
|
+
if reply.status >= 400:
|
|
249
|
+
raise _code_error(reply, retried_message=True)
|
|
250
|
+
token = reply.body.get("access_token")
|
|
251
|
+
if not token:
|
|
252
|
+
raise ServerError(text("error.no_token"), status_code=reply.status, body=reply.body)
|
|
253
|
+
return str(token)
|
|
254
|
+
|
|
255
|
+
return_to = base64.b64encode(self._settings.return_to.encode()).decode()
|
|
256
|
+
token: str = yield Op("POST", "login_url", "code_verify", parse,
|
|
257
|
+
body={"phone": number, "code": code, "return_to": return_to}, flaky=True,
|
|
258
|
+
session=False, action="Checking the SMS code")
|
|
259
|
+
profile: Profile = yield from self._sign_in({"input": {"accessToken": token}})
|
|
260
|
+
return profile
|
|
261
|
+
|
|
262
|
+
def _sign_in(self, variables: Mapping[str, Any]) -> Flow[Profile]:
|
|
263
|
+
previous = dict(self._client._jar)
|
|
264
|
+
self._client._jar = {}
|
|
265
|
+
signed = False
|
|
266
|
+
try:
|
|
267
|
+
yield _graphql(queries.SIGN_IN, _sign_in_payload, variables, name="SignIn", session=False,
|
|
268
|
+
action="Signing in")
|
|
269
|
+
if not self._client._jar:
|
|
270
|
+
raise ServerError(text("error.no_cookie"))
|
|
271
|
+
signed = True
|
|
272
|
+
profile: Profile = yield _graphql(queries.PROFILE, _profile, name="Profile", safe=True)
|
|
273
|
+
except Exception:
|
|
274
|
+
if not signed:
|
|
275
|
+
self._client._jar = previous
|
|
276
|
+
raise
|
|
277
|
+
return profile
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class Items(_Resource):
|
|
281
|
+
def create(self, item: ListingLike) -> Any:
|
|
282
|
+
data = listing(item)
|
|
283
|
+
count = len(data.photo_ids)
|
|
284
|
+
if not PHOTOS_MIN <= count <= PHOTOS_MAX:
|
|
285
|
+
raise InvalidRequestError(text("value.photos", low=PHOTOS_MIN, high=PHOTOS_MAX, count=count))
|
|
286
|
+
parse = lambda body: ItemResult.from_api(unwrap(body, "createItem", CREATE_STATUS))
|
|
287
|
+
return self._run(_graphql(queries.CREATE_ITEM, parse, {"input": data.to_create_input()}, name="CreateItem",
|
|
288
|
+
action="Creating the listing"))
|
|
289
|
+
|
|
290
|
+
def update(self, item_id: str, item: ListingLike) -> Any:
|
|
291
|
+
given = site_id(item_id, "item_id")
|
|
292
|
+
data = listing(item)
|
|
293
|
+
parse = lambda body: ItemResult.from_api(unwrap(body, "updateItem", UPDATE_STATUS))
|
|
294
|
+
return self._run(_graphql(queries.UPDATE_ITEM, parse, {"input": data.to_update_input(given)},
|
|
295
|
+
name="UpdateItem", action="Updating the listing"))
|
|
296
|
+
|
|
297
|
+
def delete(self, item_id: str) -> Any:
|
|
298
|
+
given = site_id(item_id, "item_id")
|
|
299
|
+
parse = lambda body: ItemResult.from_api(unwrap(body, "deleteItem"))
|
|
300
|
+
return self._run(_graphql(queries.DELETE_ITEM, parse, {"input": {"itemId": given}}, name="DeleteItem",
|
|
301
|
+
action="Deleting the listing"))
|
|
302
|
+
|
|
303
|
+
def prolong(self, item_id: str) -> Any:
|
|
304
|
+
given = site_id(item_id, "item_id")
|
|
305
|
+
parse = lambda body: ItemResult.from_api(unwrap(body, "prolongItem"))
|
|
306
|
+
return self._run(_graphql(queries.PROLONG_ITEM, parse, {"input": {"itemId": given}}, name="ProlongItem",
|
|
307
|
+
action="Prolonging the listing"))
|
|
308
|
+
|
|
309
|
+
def get(self, item_id: str) -> Any:
|
|
310
|
+
given = site_id(item_id, "item_id")
|
|
311
|
+
|
|
312
|
+
def parse(body: Mapping[str, Any]) -> Item:
|
|
313
|
+
edit = body.get("itemEdit")
|
|
314
|
+
found = edit.get("item") if isinstance(edit, Mapping) else None
|
|
315
|
+
if not isinstance(found, Mapping) or not found:
|
|
316
|
+
raise NotFoundError(text("error.not_found", item_id=given), status_code=404)
|
|
317
|
+
return Item.from_api(found)
|
|
318
|
+
|
|
319
|
+
return self._run(_graphql(queries.ITEM_EDIT, parse, {"id": given}, name="ItemEdit", safe=True))
|
|
320
|
+
|
|
321
|
+
def list(self, tab: TabLike = None, first: int = 20, after: Optional[str] = None) -> Any:
|
|
322
|
+
variables = {"state": item_tab(tab), "first": whole(first, "first", 1, PAGE_MAX), "after": after or None}
|
|
323
|
+
return self._run(self._page_op(variables))
|
|
324
|
+
|
|
325
|
+
def all(self, tab: TabLike = None, max_pages: int = 50) -> Any:
|
|
326
|
+
return self._flow(self._every(item_tab(tab), whole(max_pages, "max_pages", 1, 10**4)))
|
|
327
|
+
|
|
328
|
+
def _page_op(self, variables: Mapping[str, Any]) -> Op:
|
|
329
|
+
parse = lambda body: ItemPage.from_api(_company(body).get("items") or {})
|
|
330
|
+
return _graphql(queries.MY_ITEMS, parse, variables, name="MyItems", safe=True)
|
|
331
|
+
|
|
332
|
+
def _every(self, state: Optional[str], max_pages: int) -> Flow[tuple[Item, ...]]:
|
|
333
|
+
items: list[Item] = []
|
|
334
|
+
cursor: Optional[str] = None
|
|
335
|
+
for _ in range(max_pages):
|
|
336
|
+
page: ItemPage = yield self._page_op({"state": state, "first": PAGE_MAX, "after": cursor})
|
|
337
|
+
items.extend(page.items)
|
|
338
|
+
cursor = page.next_cursor
|
|
339
|
+
if not cursor:
|
|
340
|
+
break
|
|
341
|
+
return tuple(items)
|
|
342
|
+
|
|
343
|
+
def pay(self, item_id: str, *, confirm: bool = False) -> Any:
|
|
344
|
+
given = site_id(item_id, "item_id")
|
|
345
|
+
if confirm is not True:
|
|
346
|
+
raise InvalidRequestError(text("value.confirm", action="Paying for a listing"))
|
|
347
|
+
parse = lambda body: ItemResult.from_api(unwrap(body, "payForItem"))
|
|
348
|
+
return self._run(_graphql(queries.PAY_FOR_ITEM, parse, {"input": {"itemId": given}}, name="PayForItem",
|
|
349
|
+
action="Paying for the listing"))
|
|
350
|
+
|
|
351
|
+
def paid_services(self, item_id: str) -> Any:
|
|
352
|
+
return self._flow(self._services(site_id(item_id, "item_id")))
|
|
353
|
+
|
|
354
|
+
def _services(self, item_id: str) -> Flow[tuple[PaidService, ...]]:
|
|
355
|
+
ids = [item_id]
|
|
356
|
+
data: Mapping[str, Any] = yield _graphql(queries.PAID_SERVICES, lambda body: body, {"ids": ids},
|
|
357
|
+
name="PaidServices", safe=True)
|
|
358
|
+
out = []
|
|
359
|
+
for service in data.get("paidServices") or []:
|
|
360
|
+
if not isinstance(service, Mapping):
|
|
361
|
+
continue
|
|
362
|
+
details: Mapping[str, Any] = yield _graphql(queries.PAID_SERVICE_DETAILS, lambda body: body,
|
|
363
|
+
{"sid": service.get("id"), "ids": ids},
|
|
364
|
+
name="PaidServiceDetails", safe=True)
|
|
365
|
+
out.append(PaidService.from_api(service, details))
|
|
366
|
+
return tuple(out)
|
|
367
|
+
|
|
368
|
+
def buy_service(self, item_id: str, service_id: str, payment_method_id: str, *, option_id: Optional[str] = None,
|
|
369
|
+
amount: Optional[float] = None, confirm: bool = False) -> Any:
|
|
370
|
+
given = site_id(item_id, "item_id")
|
|
371
|
+
if confirm is not True:
|
|
372
|
+
raise InvalidRequestError(text("value.confirm", action="A paid service"))
|
|
373
|
+
data: dict[str, Any] = {"paidServiceId": required(service_id, "service_id", 64),
|
|
374
|
+
"paymentMethodId": required(payment_method_id, "payment_method_id", 64),
|
|
375
|
+
"targetId": [given], "platform": "DESKTOP"}
|
|
376
|
+
if option_id is not None:
|
|
377
|
+
data["paidServiceOptionId"] = required(option_id, "option_id", 64)
|
|
378
|
+
if amount is not None:
|
|
379
|
+
data["amount"] = amount
|
|
380
|
+
parse = lambda body: Payment.from_api(unwrap(body, "createPayment"))
|
|
381
|
+
return self._run(_graphql(queries.CREATE_PAYMENT, parse, {"input": data}, name="CreatePayment",
|
|
382
|
+
action="The payment"))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _photo(body: Mapping[str, Any]) -> Photo:
|
|
386
|
+
photos = body.get("photos") or []
|
|
387
|
+
first = photos[0] if isinstance(photos, list) and photos else None
|
|
388
|
+
if not isinstance(first, Mapping) or first.get("id") in (None, ""):
|
|
389
|
+
raise ServerError(text("error.photo_id"), body=body)
|
|
390
|
+
return Photo.from_api(first)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _rotated(body: Mapping[str, Any]) -> Photo:
|
|
394
|
+
if not isinstance(body, Mapping) or body.get("id") in (None, ""):
|
|
395
|
+
raise ServerError(text("error.photo_id"), body=body)
|
|
396
|
+
return Photo.from_api(body)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class Photos(_Resource):
|
|
400
|
+
def upload(self, photo: PhotoLike, *, filename: Optional[str] = None, content_type: Optional[str] = None) -> Any:
|
|
401
|
+
return self._run(self._upload_op(photo, filename, content_type))
|
|
402
|
+
|
|
403
|
+
def upload_many(self, photos: Any) -> Any:
|
|
404
|
+
ops = [self._upload_op(photo, None, None) for photo in photos]
|
|
405
|
+
return self._flow(self._many(ops))
|
|
406
|
+
|
|
407
|
+
def _many(self, ops: list[Op]) -> Flow[tuple[Photo, ...]]:
|
|
408
|
+
out = []
|
|
409
|
+
for op in ops:
|
|
410
|
+
uploaded: Photo = yield op
|
|
411
|
+
out.append(uploaded)
|
|
412
|
+
return tuple(out)
|
|
413
|
+
|
|
414
|
+
def _upload_op(self, photo: PhotoLike, filename: Optional[str], content_type: Optional[str]) -> Op:
|
|
415
|
+
name, content, kind = read_photo(photo, filename, content_type)
|
|
416
|
+
return Op("POST", "photos_url", "photo_upload", _photo, query={"lang": "az"},
|
|
417
|
+
files=[("images[]", (name, content, kind))], action="The photo upload")
|
|
418
|
+
|
|
419
|
+
def rotate(self, photo_id: str, angle: int) -> Any:
|
|
420
|
+
if angle not in PHOTO_ANGLES or isinstance(angle, bool):
|
|
421
|
+
raise InvalidRequestError(text("value.choice", field="angle", choices=PHOTO_ANGLES, value=angle))
|
|
422
|
+
query = {"id": required(photo_id, "photo_id", 300), "angle": str(angle), "lang": "az"}
|
|
423
|
+
return self._run(Op("POST", "photos_url", "photo_rotate", _rotated, query=query,
|
|
424
|
+
action="Rotating the photo"))
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _places(name: str) -> Any:
|
|
428
|
+
return lambda body: tuple(Place.from_api(p) for p in body.get(name) or [] if isinstance(p, Mapping))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class Catalog(_Resource):
|
|
432
|
+
def categories(self) -> Any:
|
|
433
|
+
parse = lambda body: tuple(CategoryNode.from_api(c) for c in body.get("categories") or []
|
|
434
|
+
if isinstance(c, Mapping))
|
|
435
|
+
return self._run(_graphql(queries.CATEGORIES, parse, safe=True, session=False))
|
|
436
|
+
|
|
437
|
+
def cities(self) -> Any:
|
|
438
|
+
return self._run(_graphql(queries.CITIES, _places("cities"), safe=True, session=False))
|
|
439
|
+
|
|
440
|
+
def regions(self, city_id: str) -> Any:
|
|
441
|
+
return self._run(_graphql(queries.REGIONS, _places("regions"), {"c": site_id(city_id, "city_id")},
|
|
442
|
+
safe=True, session=False))
|
|
443
|
+
|
|
444
|
+
def townships(self, region_id: str) -> Any:
|
|
445
|
+
return self._run(_graphql(queries.TOWNSHIPS, _places("townships"), {"r": site_id(region_id, "region_id")},
|
|
446
|
+
safe=True, session=False))
|
|
447
|
+
|
|
448
|
+
def landmarks(self, city_id: str, latitude: float, longitude: float) -> Any:
|
|
449
|
+
variables = {"c": site_id(city_id, "city_id"), "lat": float(latitude), "lng": float(longitude)}
|
|
450
|
+
return self._run(_graphql(queries.LANDMARKS, _places("landmarks"), variables, safe=True, session=False))
|