· 4 years ago · Mar 31, 2021, 12:24 PM
1import asyncio
2from datetime import datetime
3from typing import List, Dict, Any, Union, Optional, Tuple
4from glQiwiApi import HttpXParser
5
6from glQiwiApi.configs import BASE_YOOMONEY_URL, OPERATION_TRANSFER
7from glQiwiApi.data import AccountInfo, OperationType, ALL_OPERATION_TYPES, Operation
8from glQiwiApi.exceptions import NoUrlFound, InvalidData
9from glQiwiApi.utils import parse_auth_link, parse_headers, DataFormatter, measure_time, datetime_to_str_in_iso
10
11TOKEN = '4100116602400968.44E1736C18ED0C1BFF6B1A0B468CDE971A25E0006B242F71591DB8B86C1BDAEB383E5B5BFF64BB82D7EA8060BFCFCE4388D0F2166A1F64E0822DE5C7DC9AFBC2E123A4C7C06F7645F047EBBEEB0C76B56B8BAD708151933C060891C3D6C5585F24029AFD57EBAB8A82C34405CAC9D208E8A1644F2879BA4A136A244A48AB56E9'
12
13
14class YooMoney(object):
15
16 def __init__(self, api_access_token: str) -> None:
17 """
18 Конструктор принимает только токен, полученный из класс методов этого же класса
19
20 :param api_access_token: апи токен для запросов
21 """
22 self.api_access_token = api_access_token
23 self.__content_and_auth = {'content_json': True, 'auth': True}
24 self._parser = HttpXParser()
25 self._formatter = DataFormatter()
26
27 def _auth_token(self, headers: dict) -> Dict[Any, Any]:
28 headers['Authorization'] = headers['Authorization'].format(
29 token=self.api_access_token
30 )
31 return headers
32
33 @classmethod
34 async def build_url_for_auth(cls, scope: List[str], client_id: str,
35 redirect_uri: str = 'https://example.com') -> str:
36 """
37 Метод для получения ссылки для дальнейшей авторизации и получения токена yoomoney\n
38 Сценарий авторизации, взятый из документации, приложения пользователем:\n
39 1. Пользователь инициирует авторизацию приложения для управления своим счетом.\n
40 2. Приложение отправляет запрос Authorization Request на сервер ЮMoney.\n
41 3. ЮMoney перенаправляют пользователя на страницу авторизации.\n
42 4. Пользователь вводит свой логин и пароль,
43 просматривает список запрашиваемых прав и подтверждает, либо отклоняет запрос авторизации.
44 5. Приложение получает ответ Authorization Response
45 в виде HTTP Redirect со временным токеном для получения доступа или кодом ошибки.\n
46 6. Приложение, используя полученный временный токен доступа,
47 отправляет запрос на получение токена авторизации (Access Token Request).\n
48 7. Ответ содержит токен авторизации (access_token).\n
49 8. Приложение сообщает пользователю результат авторизации.\n
50 Подробно это описано в оффициальной документации:
51 https://yoomoney.ru/docs/wallet/using-api/authorization/request-access-token
52
53 :param scope: OAuth2-авторизации приложения пользователем, права перечисляются через пробел.
54 :param client_id: идентификатор приложения, тип string
55 :param redirect_uri: воронка, куда прийдет временный код, который нужен для получения основного токена
56 :return: ссылку, по которой нужно перейти и сделать авторизацию через логин/пароль
57 """
58 headers = parse_headers()
59 params = {
60 'client_id': client_id,
61 'response_type': 'code',
62 'redirect_uri': redirect_uri,
63 'scope': " ".join(scope)
64 }
65 async for response in HttpXParser().fast().fetch(
66 url=BASE_YOOMONEY_URL + '/oauth/authorize',
67 headers=headers,
68 data=params,
69 method='POST'
70 ):
71 try:
72 return parse_auth_link(response.response_data)
73 except IndexError:
74 raise NoUrlFound('Не удалось найти ссылку для авторизации в ответе от апи, проверятьте client_id')
75
76 @classmethod
77 async def get_access_token(cls, code: str, client_id: str, redirect_uri: str = 'https://example.com') -> str:
78 """
79 Метод для получения токена для запросов к YooMoney API
80
81 :param code: временный код, который был получен в методе base_authorize
82 :param client_id: идентификатор приложения, тип string
83 :param redirect_uri: воронка, куда прийдет временный код, который нужен для получения основного токена
84 :return: YooMoney API TOKEN
85 """
86 headers = parse_headers(content_json=True)
87 params = {
88 'code': code,
89 'client_id': client_id,
90 'grant_type': 'authorization_code',
91 'redirect_uri': redirect_uri
92 }
93 async for response in HttpXParser().fast().fetch(
94 url=BASE_YOOMONEY_URL + '/oauth/token',
95 headers=headers,
96 data=params,
97 method='POST',
98 get_json=True
99 ):
100 return response.response_data.get('access_token')
101
102 async def revoke_api_token(self) -> bool:
103 """
104 Метод для отзывания токена, при этом все его права тоже пропадают
105 Документация: https://yoomoney.ru/docs/wallet/using-api/authorization/revoke-access-token
106 """
107 headers = self._auth_token(parse_headers(auth=True))
108 async for response in self._parser.fast().fetch(
109 url=BASE_YOOMONEY_URL + '/api/revoke',
110 method='POST',
111 headers=headers
112 ):
113 return response.ok
114
115 async def account_info(self) -> AccountInfo:
116 """
117 Метод для получения информации об аккаунте пользователя
118 Подробная документация: https://yoomoney.ru/docs/wallet/user-account/account-info
119
120 :return: объект AccountInfo
121 """
122 headers = self._auth_token(parse_headers(**self.__content_and_auth))
123 async for response in self._parser.fast().fetch(
124 url=BASE_YOOMONEY_URL + '/api/account-info',
125 headers=headers,
126 method='POST'
127 ):
128 try:
129 return self._formatter.format_objects(
130 iterable_obj=(response.response_data,),
131 obj=AccountInfo
132 )[0]
133 except IndexError:
134 raise InvalidData('Cannot fetch account info, check your token')
135
136 async def get_operation_history(
137 self,
138 operation_types: Optional[Union[List[OperationType], Tuple[OperationType]]] = None,
139 start_date: Optional[datetime] = None,
140 end_date: Optional[datetime] = None,
141 start_record: Optional[int] = None,
142 records: int = 30,
143 label: Optional[Union[str, int]] = None
144 ):
145 """
146 Подробная документация: https://yoomoney.ru/docs/wallet/user-account/operation-history\n
147 Метод позволяет просматривать историю операций (полностью или частично) в постраничном режиме.\n
148 Записи истории выдаются в обратном хронологическом порядке: от последних к более ранним.\n
149 Перечень типов операций, которые требуется отобразить. Возможные значения:\n
150 DEPOSITION — пополнение счета (приход);\n
151 PAYMENT — платежи со счета (расход);\n
152 INCOMING(incoming-transfers-unaccepted) — непринятые входящие P2P-переводы любого типа.\n
153
154 :param operation_types: Тип операций
155 :param label: string.
156 Отбор платежей по значению метки. Выбираются платежи, у которых указано заданное значение параметра label вызова request-payment.
157 :param start_date: datetime Вывести операции от момента времени (операции, равные start_date, или более поздние).
158 Если параметр отсутствует, выводятся все операции.
159 :param end_date: datetime Вывести операции до момента времени (операции более ранние, чем end_date).
160 Если параметр отсутствует, выводятся все операции.
161 :param start_record: string Если параметр присутствует, то будут отображены операции, начиная с номера start_record.
162 Операции нумеруются с 0. Подробнее про постраничный вывод списка
163 :param records: int Количество запрашиваемых записей истории операций. Допустимые значения: от 1 до 100, по умолчанию — 30.
164 """
165 headers = self._auth_token(parse_headers(**self.__content_and_auth))
166
167 data = {
168 'records': records,
169 }
170
171 if operation_types:
172 if all(isinstance(operation_type, OperationType) for operation_type in operation_types):
173 data.update({'type': ' '.join([operation_type.value for operation_type in operation_types])})
174
175 if records <= 0 or records > 100:
176 raise InvalidData(
177 'Неверное количество записей. '
178 'Кол-во записей, которые можно запросить, находиться в диапазоне от 1 до 100 включительно'
179 )
180
181 if isinstance(start_record, int):
182 if start_record < 0:
183 raise InvalidData('Укажите позитивное число')
184 data.update({'start_record': start_record})
185
186 data.update({'label': label}) if isinstance(label, str) else None
187 if start_date:
188 if not isinstance(start_date, datetime):
189 raise InvalidData('Параметр start_date был передан неправильным типом данных')
190 data.update({'from': datetime_to_str_in_iso(start_date)})
191
192 if end_date:
193 if not isinstance(end_date, datetime):
194 raise InvalidData('Параметр end_date был передан неправильным типом данных')
195 data.update({'till': datetime_to_str_in_iso(end_date)})
196
197 async for response in self._parser.fast().fetch(
198 url=BASE_YOOMONEY_URL + '/api/operation-history',
199 method='POST',
200 headers=headers,
201 data=data,
202 get_json=True
203 ):
204 return self._formatter.format_objects(
205 iterable_obj=response.response_data.get('operations'),
206 obj=Operation,
207 transfers=OPERATION_TRANSFER
208 )
209
210
211@measure_time
212async def main():
213 wallet = YooMoney(
214 api_access_token=TOKEN
215 )
216 history = await wallet.get_operation_history(
217 records=5,
218 operation_types=[OperationType.PAYMENT],
219 )
220 print(history)
221
222
223if __name__ == '__main__':
224 asyncio.run(main())
225