From 78049deffd3bc5174d08dd30d60e0f2ab9d80910 Mon Sep 17 00:00:00 2001 From: ssleg <1537206@gmail.com> Date: Mon, 17 May 2021 14:52:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B5=D1=80=D1=81=D0=B8=D1=8F=201.10,=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B2=D1=85=D0=BE=D0=B4=D0=BD=D1=8B=D1=85=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B1=D0=BD=D1=8B=D0=B9=20=D0=BB=D0=BE=D0=B3=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA.=20=D0=A2=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=B6=D0=B5=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 9 ++ README.MD | 29 ++++++ adv_sample.py | 54 ++++++++++ qiwi_key.txt | 1 + qiwi_module.py | 257 ++++++++++++++++++++++++++++++++++++++--------- requirements.txt | 1 + sample.py | 42 ++++++++ 7 files changed, 343 insertions(+), 50 deletions(-) create mode 100644 LICENSE create mode 100644 README.MD create mode 100644 adv_sample.py create mode 100644 qiwi_key.txt create mode 100644 requirements.txt create mode 100644 sample.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f0e0fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License + +Copyright ©2020-2021 https://t.me/ssleg + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..f952b51 --- /dev/null +++ b/README.MD @@ -0,0 +1,29 @@ +###### Модуль для работы с API Qiwi-кошелька. + +Предназначен для использования в python проектах, в том числе в **telegram-ботах**. +Работает с Qiwi-кошельками **физических** лиц. +Позволяет выставлять счета клиентам и контролировать их оплату. + +Содержит три функции: +`create_bill` - создание счета. +`bill_status` - проверка оплаты. +`cancel_bill` - отмена счета. + +Поддерживается только метод **поллинга** (опроса) сервера. + +Примеры использования модуля смотрите в файлах **sample.py** (простой) и **adv_sample.py** (расширенный). + +Перед использованием необходимо ваш **секретный ключ** записать в файл qiwi_key.txt. +Он должен всегда быть в той же папке, в которую вы положите сам модуль. + +Начать прием переводов на свой кошелек вы [https://p2p.qiwi.com][можете здесь]. + +Полная документация по API Qiwi [https://developer.qiwi.com/ru/p2p-payments/#API][лежит здесь]. + +Благодарности, вопросы и замечания складывать в комментариях к [https://t.me/ssleg/321][этому посту]. + +Лицензия на код и документацию MIT. +Вы можете свободно использовать, изменять и продавать код при условии сохранения информации об авторских правах. + + + diff --git a/adv_sample.py b/adv_sample.py new file mode 100644 index 0000000..7904d1e --- /dev/null +++ b/adv_sample.py @@ -0,0 +1,54 @@ +# Qiwi module advanced usage example v1.00 +# 17/05/2021 +# https://t.me/ssleg © 2021 + +import logging + +import qiwi_module + +# настройка логфлайла test,log, туда будут записываться все ошибки и предупреждения. +lfile = logging.FileHandler('test.log', 'a', 'utf-8') +lfile.setFormatter(logging.Formatter('%(levelname)s %(module)-13s [%(asctime)s] %(message)s')) +# noinspection PyArgumentList +logging.basicConfig(level=logging.INFO, handlers=[lfile]) + +# простой вариант использования смотрите в файле sample.py + +# если у вас настроен свой внешний вид формы платежа, необходимо передать код темы модулю. +# это делается один раз, при его инициализации. +# сам код и настройки формы находятся на странице https://qiwi.com/p2p-admin/transfers/link +theme_code = 'Ivanov-XX-vvv-k_' + +# перед любым использованием необходима однократная инициализация модуля. +qiwi_module.init(theme_code) + +# создание счета на 1 рубль. При успехе получаете url с формой оплаты для клиента. +# при неуспехе возвращается False с подробной записью в лог. + +# идентификаторы счетов придумываете и сохраняете вы сами, они должны быть уникальными всегда. +bill_id = 'bill_2021_00000002' + +# по умолчанию счет действителен 15 минут, но вы можете поставить свое время, например сутки и 1 минуту. +valid_hours = 24 +valid_minutes = 1 + +# есть так же поле для комментария, его видит клиент в форме оплаты. например, туда можно записать детали заказа +comment = 'Винт с левой резьбой для Сидорова.' + +invoice_url = qiwi_module.create_bill(1.00, bill_id, comment, valid_hours, valid_minutes) +print(invoice_url) + +# проверка статуса оплаты. +# возвращает одно из четырех возможных значений, если успешно или False и запись в лог. +# 'WAITING' - cчет выставлен, ожидает оплаты. +# 'PAID' - cчет оплачен. +# 'REJECTED' - счет отменен с вашей стороны. +# 'EXPIRED' - счет не оплачен и истек срок его действия. +# можно вызывать ежесекундно или реже. +pay_status = qiwi_module.bill_status(bill_id) +print(pay_status) + +# отмена счета, если вам это необходимо. +# возврашает 'REJECTED' если успешно, иначе False и запись в лог. +bill_status = qiwi_module.cancel_bill(bill_id) +print(bill_status) diff --git a/qiwi_key.txt b/qiwi_key.txt new file mode 100644 index 0000000..95f018d --- /dev/null +++ b/qiwi_key.txt @@ -0,0 +1 @@ +iQJJBAABCgAzFiEE6Gl+Lu92wC06YzJ3iIGyqCEJdvIFAmCBVw4VHHBhY2thZ2VzQHBnYWRtaW4ub3JnAAoJEIiBsqghCXbyDY8P/i33M99WWx0XGJDNtJMThse7ABCjocamwsVlBQ1IxbTqD26s3MviUzDN337XqwAWz6N6h5hPEzGYe9iI0QErIKsvsZpZJJrEJiLNamA1a diff --git a/qiwi_module.py b/qiwi_module.py index fad748d..a9b160b 100644 --- a/qiwi_module.py +++ b/qiwi_module.py @@ -1,89 +1,246 @@ -# Qiwi module v1.00 -# 21/08/2020 -# https://t.me/ssleg © 2020 +# Qiwi module v1.10 +# 17/05/2021 +# https://t.me/ssleg © 2020 – 2021 + +import logging +from datetime import datetime, timedelta +from pathlib import Path import requests -import logging headers = { 'accept': 'application/json', - 'content-type': 'application/json', - 'Authorization': 'Bearer xxxxxx' # ваш секретный ключ из личного кабинета + 'content-type': 'application/json' } url = 'https://api.qiwi.com/partner/bill/v1/bills/' +error_flag = True +theme_code = None -# создание платежа. на входе; сумма (число), номер заказа (текст, идет в комментарий к платежу) -# номер счета (текст, любой уникальный счетчик) и str(datetime) формата 2020-08-21 10:34:22 -# на выходе возвращает URL формы оплаты для клиента. -def create_bill(summa, order_num, bill_num, exp_datetime): - am = {'currency': 'RUB', 'value': '{:.2f}'.format(summa)} - exp = exp_datetime.replace(' ', 'T') + '+03:00' - # персональная форма платежа, может не использоваться - # cust = {'themeCode': 'ваш код формы из личного кабинета'} - rdata = {'amount': am, 'expirationDateTime': exp, 'comment': order_num} # , 'customFields': cust} - rurl = url + bill_num +# не подлежит прямому вызову. +# проверяет, что модуль инициализирован. +def init_check(): + if error_flag: + levent = 'module is not initialized.' + logging.error(levent) + return False + return True + + +# не подлежит прямому вызову. +# проверяет корректность суммы счета. +def zero_check(amount): + if type(amount) == int or type(amount) == float: + if amount <= 0: + levent = 'bill amount is zero or negative.' + logging.error(levent) + return False + return True + else: + levent = 'bill amount is not a number. positive int or float values possible.' + logging.error(levent) + return False + + +# не подлежит прямому вызову. +# возврашает строку со временем действия счета в формате сервера Qiwi. +def get_valid_to_time(hours, mins): + if type(hours) == int and type(mins) == int: + if hours >= 0 and mins > 0: + now = datetime.now() + delta = timedelta(hours=hours, minutes=mins) + valid_to = now + delta + valid_to_string = str(valid_to)[0:19] + rezult_string = valid_to_string.replace(' ', 'T') + '+03:00' + return rezult_string + else: + levent = 'hours or minutes must be above zero.' + logging.error(levent) + return False + else: + levent = 'hours or minutes is not integer.' + logging.error(levent) + return False + + +# создание счета на оплату. при успехе возвращает url с формой оплаты для клиента. +# при неуспехе - False. +# примеры использования смотрите в sample.py и adv_sample.py +def create_bill(bill_amount, bill_id, comment_string=None, valid_hours=0, valid_mins=15): + if not init_check(): + return False + + if not zero_check(bill_amount): + return False + + if type(bill_amount) == float: + full_penny = bill_amount * 100 + fract = full_penny - int(full_penny) + if fract != 0: + levent = 'bill amount must have integer number of penny (kopeek).' + logging.error(levent) + return False + + if type(bill_id) != str or bill_id == '': + levent = 'bill id is not a string or empty.' + logging.error(levent) + return False + + valid_to = get_valid_to_time(valid_hours, valid_mins) + if not valid_to: + return False + + amount = {'currency': 'RUB', 'value': '{:.2f}'.format(bill_amount)} + + request_data = {'amount': amount, 'expirationDateTime': valid_to} + + if comment_string is not None: + if type(comment_string) == str and comment_string != '': + request_data['comment'] = comment_string + else: + levent = 'comment is not a string or empty.' + logging.warning(levent) + + if theme_code is not None: + custom = {'themeCode': theme_code} + request_data['customFields'] = custom + + request_url = url + bill_id try: - response = requests.put(rurl, json=rdata, headers=headers, timeout=5) - cod = response.status_code - res = response.json() - if cod == 200: - return res.get('payUrl') - else: - levent = 'qiwi server error (create bill). code - ' + str(cod) + ', response - ' + str(res) + response = requests.put(request_url, json=request_data, headers=headers, timeout=5) + response_code = response.status_code + if response_code == 200: + response_dict = response.json() + return response_dict.get('payUrl') + elif response_code == 401: + levent = 'Qiwi autorization error. invalid secret key.' logging.error(levent) - return 'error' + return False + else: + response_text = response.text + levent = 'Qiwi server error (create bill). code - ' + str(response_code) + ', response - ' + response_text + logging.error(levent) + return False except Exception as e: levent = 'protocol error (create bill): ' + str(e) logging.error(levent) - return 'error' + return False -# проверка статуса платежа,на входе его номер (текст), на выходе статус из документации. +# проверка статуса счета,на входе его идентификатор (текст), на выходе один из 4х статусов, если успешно: +# 'WAITING' - cчет выставлен, ожидает оплаты. +# 'PAID' - cчет оплачен. +# 'REJECTED' - счет отменен. +# 'EXPIRED' - счет не оплачен и истек срок его действия. # можно вызывать 1 раз в секунду и реже. -def bill_status(bill_num): - rurl = url + bill_num +# если неуспешно - возвращает False. +def bill_status(bill_id): + if not init_check(): + return False + + if type(bill_id) != str or bill_id == '': + levent = 'bill id is not a string or empty.' + logging.error(levent) + return False + + request_url = url + bill_id try: - response = requests.get(rurl, headers=headers, timeout=5) - cod = response.status_code - res = response.json() - if cod == 200: - status = res.get('status') + response = requests.get(request_url, headers=headers, timeout=5) + response_code = response.status_code + if response_code == 200: + response_dict = response.json() + status = response_dict.get('status') return status.get('value') - else: - levent = 'qiwi server error (bill status). code - ' + str(cod) + ', response - ' + str(res) + elif response_code == 401: + levent = 'Qiwi autorization error. invalid secret key.' logging.error(levent) - return 'error' + return False + else: + response_text = response.text + levent = 'Qiwi server error (bill status). code - ' + str(response_code) + ', response - ' + response_text + logging.error(levent) + return False except Exception as e: levent = 'protocol error (bill status): ' + str(e) logging.error(levent) - return 'error' + return False -# отмена счета, на входе его номер (текст). -# в случае успеха возвращает REJECTED -def cancel_bill(bill_num): - rurl = url + bill_num + '/reject' +# отмена счета, на входе его идентификатор (текст). +# в случае успеха возвращает REJECTED, иначе False +def cancel_bill(bill_id): + if not init_check(): + return False + + if type(bill_id) != str or bill_id == '': + levent = 'bill id is not a string or empty.' + logging.error(levent) + return False + + request_url = url + bill_id + '/reject' try: - response = requests.post(rurl, headers=headers, timeout=5) - cod = response.status_code - res = response.json() - if cod == 200: - status = res.get('status') + response = requests.post(request_url, headers=headers, timeout=5) + response_code = response.status_code + if response_code == 200: + response_dict = response.json() + status = response_dict.get('status') return status.get('value') - else: - levent = 'qiwi server error (cancel bill). code - ' + str(cod) + ', response - ' + str(res) + elif response_code == 401: + levent = 'Qiwi autorization error. invalid secret key.' logging.error(levent) - return 'error' + return False + else: + response_text = response.text + levent = 'Qiwi server error (cancel bill). code - ' + str(response_code) + ', response - ' + response_text + logging.error(levent) + return False except Exception as e: levent = 'protocol error (cancel bill): ' + str(e) logging.error(levent) - return 'error' + return False + + +# инициализация модуля - загрузка секретного ключа из файла и настройка пользовательской темы для формы оплаты. +def init(theme=None): + global error_flag + global theme_code + + if theme is not None: + if type(theme) == str and theme != '': + theme_code = theme + else: + levent = 'custom theme code is not a string or empty. theme not used.' + logging.warning(levent) + + key_path = Path('qiwi_key.txt') + if not key_path.exists(): + levent = 'Qiwi key file not found, module work is not possible. write secret key to file qiwi_key.txt' + logging.error(levent) + return False + + file = open('qiwi_key.txt') + qiwi_key = file.readline() + file.close() + + if qiwi_key.find('\n') > -1: + qiwi_key = qiwi_key[0:len(qiwi_key) - 1] + + if not 180 < len(qiwi_key) < 230: + levent = 'Qiwi key not found in file, module work is not possible. write secret key to file qiwi_key.txt' + logging.error(levent) + return False + + headers['Authorization'] = 'Bearer ' + qiwi_key + + levent = 'Qiwi key loaded, init completed.' + logging.info(levent) + error_flag = False + return True diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..14bd43f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests~=2.23.0 \ No newline at end of file diff --git a/sample.py b/sample.py new file mode 100644 index 0000000..1f37dd1 --- /dev/null +++ b/sample.py @@ -0,0 +1,42 @@ +# Qiwi module usage example v1.00 +# 17/05/2021 +# https://t.me/ssleg © 2021 + +import logging + +import qiwi_module + +# настройка логфлайла test,log, туда будут записываться все ошибки и предупреждения. +lfile = logging.FileHandler('test.log', 'a', 'utf-8') +lfile.setFormatter(logging.Formatter('%(levelname)s %(module)-13s [%(asctime)s] %(message)s')) +# noinspection PyArgumentList +logging.basicConfig(level=logging.INFO, handlers=[lfile]) + +# перед любым использованием необходима однократная инициализация модуля. +qiwi_module.init() + +# создание счета на 1 рубль. При успехе получаете url с формой оплаты для клиента. +# при неуспехе возвращается False с подробной записью в лог. + +# идентификаторы счетов придумываете и сохраняете вы сами, они должны быть уникальными всегда. +bill_id = 'bill_2021_00000001' + +# по умолчанию счет действителен 15 минут. +# продвинутые варианты использования смотрите в файле adv_sample.py +invoice_url = qiwi_module.create_bill(1.00, bill_id) +print(invoice_url) + +# проверка статуса оплаты. +# возвращает одно из четырех возможных значений, если успешно или False и запись в лог. +# 'WAITING' - cчет выставлен, ожидает оплаты. +# 'PAID' - cчет оплачен. +# 'REJECTED' - счет отменен с вашей стороны. +# 'EXPIRED' - счет не оплачен и истек срок его действия. +# можно вызывать ежесекундно или реже. +pay_status = qiwi_module.bill_status(bill_id) +print(pay_status) + +# отмена счета, если вам это необходимо. +# возврашает 'REJECTED' если успешно, иначе False и запись в лог. +bill_status = qiwi_module.cancel_bill(bill_id) +print(bill_status)