├── .coveragerc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── .pylintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST ├── README.md ├── culqi ├── __init__.py ├── client.py ├── resources │ ├── __init__.py │ ├── base.py │ ├── card.py │ ├── charge.py │ ├── customer.py │ ├── event.py │ ├── iin.py │ ├── order.py │ ├── plan.py │ ├── refund.py │ ├── subscription.py │ ├── token.py │ └── transfer.py ├── utils │ ├── __init__.py │ ├── constants.py │ ├── encoder.py │ ├── errors.py │ ├── status_codes.py │ ├── urls.py │ └── validation │ │ ├── card_validation.py │ │ ├── charge_validation.py │ │ ├── country_codes.py │ │ ├── customer_validation.py │ │ ├── helpers.py │ │ ├── order_validation.py │ │ ├── plan_validation.py │ │ ├── refund_validation.py │ │ ├── subscription_validation.py │ │ └── token_validation.py └── version.py ├── poetry.lock ├── pyproject.toml ├── requirements.dev.txt ├── requirements.tox.txt ├── requirements.txt ├── resources ├── carbon.png └── logo.png ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── data.py ├── test_card.py ├── test_charge.py ├── test_client.py ├── test_customer.py ├── test_event.py ├── test_iin.py ├── test_order.py ├── test_plan.py ├── test_refund.py ├── test_subscription.py ├── test_token.py ├── test_transfer.py └── test_version.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | */.tox/* 5 | */.local/* 6 | */.cache/* 7 | */tests/* 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.coveragerc 3 | !.gitignore 4 | !.isort.cfg 5 | !.pre-commit-config.yaml 6 | !.pylintrc 7 | !.travis.yml 8 | *.py[cod] 9 | *.ipynb* 10 | *.egg* 11 | coverage.* 12 | cassettes 13 | legacy 14 | build 15 | dist 16 | __pycache__ 17 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | known_third_party = dotenv,jsonschema,pytest,requests,setuptools 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: 19.10b0 4 | hooks: 5 | - id: black 6 | language_version: python3.8 7 | 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v2.2.3 10 | hooks: 11 | - id: trailing-whitespace 12 | - id: end-of-file-fixer 13 | - id: flake8 14 | 15 | - repo: https://github.com/asottile/seed-isort-config 16 | rev: v1.9.2 17 | hooks: 18 | - id: seed-isort-config 19 | 20 | - repo: https://github.com/pre-commit/mirrors-isort 21 | rev: v4.3.21 22 | hooks: 23 | - id: isort 24 | 25 | - repo: https://github.com/pycqa/pydocstyle 26 | rev: 4.0.0 27 | hooks: 28 | - id: pydocstyle 29 | files: ^culqi/ 30 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | load-plugins=pylint.extensions.docparams, pylint.extensions.docstyle, pylint.extensions.bad_builtin, pylint.extensions.mccabe 3 | 4 | [MESSAGES CONTROL] 5 | disable=C0103, C0111, C0199, C301, C0303, C0412, C1001, E1004, I0011, R0903, W0232, W0621, W0223, C0301, W0511 6 | 7 | [DESIGN] 8 | max-parents=5 9 | max-complexity=5 10 | 11 | [SIMILARITIES] 12 | ignore-imports=yes 13 | min-similarity-lines=8 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.5' 5 | - '3.6' 6 | - '3.7' 7 | - '3.8-dev' 8 | install: 9 | - pip install tox-travis 10 | script: 11 | - tox 12 | matrix: 13 | allow_failures: 14 | - python: '3.7' 15 | - python: '3.8-dev' 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.0.0 15-08-2023 2 | 3 | - Source code improvements 4 | - Add HTTP status code 5 | - Add RSA encrypt 6 | 7 | ### v0.2.9 15-01-2023 8 | 9 | - Added support forn orderconfirmtipo 10 | - Added support for create token yape 11 | 12 | ### In progress 13 | 14 | - Drop support for python 3.4 15 | - Added support for [`orders`](https://www.culqi.com/api/#/ordenes) 16 | - Added complete set of tests 17 | 18 | ##### Breaking changes 19 | 20 | - Complete refactor in API Client 21 | - Use Client class instead to configure module directly 22 | 23 | ##### Development environment 24 | 25 | - Moved to [poetry](https://poetry.eustace.io) for dependency management 26 | - Use of [black](https://black.readthedocs.io/en/stable/) for linting 27 | - Use of [tox](https://tox.readthedocs.io/en/latest/) for testing environments 28 | - Added precomit hooks with [pre-comit](https://pre-commit.com/) 29 | - Added code [coverage](https://coverage.readthedocs.io/en/stable/) 30 | 31 | ### 0.2.5 22-02-2017 32 | 33 | - Change the default timeout of GET method 120 to 360 34 | - rename COD_COMMERCE to public_key and API_KEY to private_key 35 | 36 | ### 0.2.3 13-02-2017 37 | 38 | - Fix capture method in Charge 39 | 40 | ### 0.2.2 13-02-2017 41 | 42 | - Add Card 43 | - Add Customer 44 | - Add Event 45 | - Add Transfer 46 | - Add update method 47 | 48 | ### 0.2.1 26-01-2017 49 | 50 | - Add: LIST, GET, DELETE for each Resource 51 | 52 | ### 0.1.8 17-01-2017 53 | 54 | - Fix Create Token 55 | 56 | ### 0.1 05-01-2017 57 | 58 | - Create Token 59 | - Create Charge 60 | - Create Plan 61 | - Create Subscription 62 | - Create Refund 63 | - Unit Tests 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | > Si estas interesado en contribuir con el desarrollo y mantenimiento de este paquete 2 | es recomendable que emplees [poetry](https://poetry.eustace.io) para la gestión de 3 | dependencias. 4 | 5 | ## Entorno 6 | 7 | Clona el proyecto 8 | 9 | ```bash 10 | $ git clone https://github.com/culqi/culqi.git 11 | $ cd culqi 12 | ``` 13 | 14 | Instala las dependencias 15 | 16 | ```bash 17 | $ poetry install 18 | ``` 19 | 20 | ## Testing and coverage 21 | 22 | Puedes ejecutar los tests con poetry 23 | 24 | ```bash 25 | poetry run pytest --cov --cov-report= 26 | poetry run coverage report 27 | ``` 28 | 29 | ## ¿Quieres enviar un PR? 30 | 31 | Antes de hacer tu primer commit y enviar tu pull request ejecuta 32 | 33 | ```bash 34 | $ poetry run pre-commit install 35 | ``` 36 | 37 | Luego realiza tu commits de forma habitual. 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CULQI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | culqi/__init__.py 5 | culqi/resource.py 6 | culqi/utils.py 7 | test/test.py 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Culqi-Python 2 | 3 | [![Build Status](https://travis-ci.org/culqi/culqi-python.svg?branch=master)](https://travis-ci.org/culqi/culqi-python) 4 | ![](https://img.shields.io/pypi/pyversions/Culqi) 5 | ![](https://img.shields.io/pypi/l/culqi) 6 | ![](https://img.shields.io/pypi/v/culqi) 7 | 8 | 9 | Nuestra Biblioteca PYTHON oficial, es compatible con la v2.0 del Culqi API, con el cual tendrás la posibilidad de realizar cobros con tarjetas de débito y crédito, Yape, PagoEfectivo, billeteras móviles y Cuotéalo con solo unos simples pasos de configuración. 10 | 11 | Nuestra biblioteca te da la posibilidad de capturar el `status_code` de la solicitud HTTP que se realiza al API de Culqi, así como el `response` que contiene el cuerpo de la respuesta obtenida. 12 | 13 | ## Requisitos 14 | 15 | - Python 2.7+ 16 | * Afiliate [aquí](https://afiliate.culqi.com/). 17 | * Si vas a realizar pruebas obtén tus llaves desde [aquí](https://integ-panel.culqi.com/#/registro), si vas a realizar transacciones reales obtén tus llaves desde [aquí](https://panel.culqi.com/#/registro). 18 | 19 | > Recuerda que para obtener tus llaves debes ingresar a tu CulqiPanel > Desarrollo > ***API Keys***. 20 | 21 | ![alt tag](http://i.imgur.com/NhE6mS9.png) 22 | 23 | > Recuerda que las credenciales son enviadas al correo que registraste en el proceso de afiliación. 24 | 25 | * Para encriptar el payload debes generar un id y llave RSA ingresando a CulqiPanel > Desarrollo > RSA Keys. 26 | 27 | ## Instalación 28 | 29 | Ejecuta los siguientes comandos: 30 | 31 | ```bash 32 | py -m pip install pytest 33 | py -m pip install python-dotenv 34 | py -m pip install culqi 35 | py -m pip install jsonschema 36 | py -m pip install pycryptodome 37 | 38 | ``` 39 | 40 | ## Configuracion 41 | 42 | Para empezar a enviar peticiones al API de Culqi debes configurar tu llave pública (pk), llave privada (sk). 43 | Para habilitar encriptación de payload debes configurar tu rsa_id y rsa_public_key. 44 | El parámetro custom_headers es opcional y define los headers personalizados que se enviarán en la solicitud HTTP. Está presente solo en los métodos create. 45 | 46 | ```python 47 | 48 | from dotenv import load_dotenv 49 | from culqi2 import __version__ 50 | from culqi2.client import Culqi 51 | 52 | self.public_key = "pk_test_e94078b9b248675d" 53 | self.private_key = "sk_test_c2267b5b262745f0" 54 | self.culqi = Culqi(self.public_key, self.private_key) 55 | 56 | #ecnrypt variables 57 | self.rsa_public_key = "-----BEGIN PUBLIC KEY-----\n" + \ 58 | "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDswQycch0x/7GZ0oFojkWCYv+g\n" + \ 59 | "r5CyfBKXc3Izq+btIEMCrkDrIsz4Lnl5E3FSD7/htFn1oE84SaDKl5DgbNoev3pM\n" + \ 60 | "C7MDDgdCFrHODOp7aXwjG8NaiCbiymyBglXyEN28hLvgHpvZmAn6KFo0lMGuKnz8\n" + \ 61 | "iuTfpBl6HpD6+02SQIDAQAB\n" + \ 62 | "-----END PUBLIC KEY-----" 63 | self.rsa_id = "de35e120-e297-4b96-97ef-10a43423ddec" 64 | 65 | ``` 66 | 67 | ### Encriptar payload 68 | 69 | Para encriptar el payload necesitas agregar el parámetros **options** que contiene tu id y llave RSA. 70 | 71 | Ejemplo 72 | 73 | ```python 74 | options = {} 75 | options["rsa_public_key"] = self.rsa_public_key #"la llave pública RSA" 76 | options["rsa_id"] = self.rsa_id # "el id de tu llave" 77 | token = self.token.create(data=self.token_data, **options) 78 | 79 | ``` 80 | 81 | ## Servicios 82 | 83 | ### Crear Token 84 | 85 | Antes de crear un Cargo o Card es necesario crear un `token` de tarjeta. 86 | Lo recomendable es generar los 'tokens' con [Culqi Checkout v4](https://docs.culqi.com/es/documentacion/checkout/v4/culqi-checkout/) o [Culqi JS v4](https://docs.culqi.com/es/documentacion/culqi-js/v4/culqi-js/) **debido a que es muy importante que los datos de tarjeta sean enviados desde el dispositivo de tus clientes directamente a los servidores de Culqi**, para no poner en riesgo los datos sensibles de la tarjeta de crédito/débito. 87 | 88 | > Recuerda que cuando interactúas directamente con el [API Token](https://apidocs.culqi.com/#tag/Tokens/operation/crear-token) necesitas cumplir la normativa de PCI DSS 3.2. Por ello, te pedimos que llenes el [formulario SAQ-D](https://listings.pcisecuritystandards.org/documents/SAQ_D_v3_Merchant.pdf) y lo envíes al buzón de riesgos Culqi. 89 | 90 | ```python 91 | 92 | token = self.token.create(data=self.token_data) 93 | 94 | ``` 95 | 96 | ### Crear Cargo 97 | 98 | Crear un cargo significa cobrar una venta a una tarjeta. Para esto previamente deberías generar el `token` y enviarlo en parámetro **source_id**. 99 | 100 | Los cargos pueden ser creados vía [API de devolución](https://apidocs.culqi.com/#tag/Cargos/operation/crear-cargo). 101 | El parámetro custom_headers es opcional y define los headers personalizados que se enviarán en la solicitud HTTP. 102 | 103 | ```python 104 | charge = self.charge.create(data=self.charge_data) 105 | ``` 106 | 107 | Para realizar un cargo recurrente, puedes utilizar el siguiente código: 108 | ```python 109 | charge = self.charge.create(data=self.charge_data, custom_headers={'X-Charge-Channel': 'recurrent'}) 110 | ``` 111 | 112 | ### Crear Devolución 113 | 114 | Solicita la devolución de las compras de tus clientes (parcial o total) de forma gratuita a través del API y CulqiPanel. 115 | 116 | Las devoluciones pueden ser creados vía [API de devolución](https://apidocs.culqi.com/#tag/Devoluciones/operation/crear-devolucion). 117 | 118 | ```python 119 | refund = self.refund.create(data=self.refund_data) 120 | ``` 121 | 122 | ### Crear Customer 123 | 124 | El **cliente** es un servicio que te permite guardar la información de tus clientes. Es un paso necesario para generar una [tarjeta](/es/documentacion/pagos-online/recurrencia/one-click/tarjetas). 125 | 126 | Los clientes pueden ser creados vía [API de cliente](https://apidocs.culqi.com/#tag/Clientes/operation/crear-cliente). 127 | 128 | ```python 129 | customer = self.customer.create(data=self.customer_data) 130 | ``` 131 | 132 | ### Actualizar Customer 133 | 134 | ```python 135 | updated_customer = self.customer.update( 136 | id_=created_customer["data"]["id"], data=metadatada 137 | ) 138 | ``` 139 | 140 | ### Obtener Customer 141 | 142 | ```python 143 | retrieved_customer = self.customer.read(created_customer["data"]["id"]) 144 | ``` 145 | 146 | ### Crear Card 147 | 148 | La **tarjeta** es un servicio que te permite guardar la información de las tarjetas de crédito o débito de tus clientes para luego realizarles cargos one click o recurrentes (cargos posteriores sin que tus clientes vuelvan a ingresar los datos de su tarjeta). 149 | 150 | Las tarjetas pueden ser creadas vía [API de tarjeta](https://apidocs.culqi.com/#tag/Tarjetas/operation/crear-tarjeta). 151 | 152 | ```python 153 | card = self.card.create(data=self.card_data) 154 | ``` 155 | 156 | ### Crear Plan 157 | 158 | El plan es un servicio que te permite definir con qué frecuencia deseas realizar cobros a tus clientes. 159 | 160 | Un plan define el comportamiento de las suscripciones. Los planes pueden ser creados vía el [API de Plan](https://apidocs.culqi.com/#/planes#create) o desde el **CulqiPanel**. 161 | 162 | ```python 163 | plan = self.plan.create(data=self.plan_data) 164 | ``` 165 | 166 | ### Crear Suscripción 167 | 168 | La suscripción es un servicio que asocia la tarjeta de un cliente con un plan establecido por el comercio. 169 | 170 | Las suscripciones pueden ser creadas vía [API de suscripción](https://apidocs.culqi.com/#tag/Suscripciones/operation/crear-suscripcion). 171 | 172 | ```python 173 | subscription = self.subscription.create(data=self.subscription_data) 174 | ``` 175 | 176 | ### Crear Orden 177 | 178 | Es un servicio que te permite generar una orden de pago para una compra potencial. 179 | La orden contiene la información necesaria para la venta y es usado por el sistema de **PagoEfectivo** para realizar los pagos diferidos. 180 | 181 | Las órdenes pueden ser creadas vía [API de orden](https://apidocs.culqi.com/#tag/Ordenes/operation/crear-orden). 182 | 183 | ```python 184 | orden = self.orden.create(data=self.orden_data) 185 | ``` 186 | 187 | ## Pruebas 188 | 189 | En la caperta **/test** econtraras ejemplo para crear un token, charge, plan, órdenes, card, suscripciones, etc. 190 | 191 | > Recuerda que si quieres probar tu integración, puedes utilizar nuestras [tarjetas de prueba.](https://docs.culqi.com/es/documentacion/pagos-online/tarjetas-de-prueba/) 192 | 193 | ### Ejemplo Prueba Token 194 | 195 | ```python 196 | @pytest.mark.vcr() 197 | def test_token_create(self): 198 | token = self.token.create(data=self.token_data) 199 | print(token) 200 | assert token["data"]["object"] == "token" 201 | 202 | ``` 203 | 204 | ### Ejemplo Prueba Cargo 205 | ```python 206 | @property 207 | def charge_data(self): 208 | # pylint: disable=no-member 209 | token_data = deepcopy(Data.TOKEN) 210 | token = self.culqi.token.create(data=token_data) 211 | print(token) 212 | charge_data = deepcopy(Data.CHARGE) 213 | charge_data["source_id"] = token["data"]["id"] 214 | 215 | return charge_data 216 | 217 | @pytest.mark.vcr() 218 | def test_charge_create(self): 219 | charge = self.charge.create(data=self.charge_data) 220 | print (charge) 221 | assert charge["data"]["object"] == "charge" 222 | ``` 223 | 224 | ## Documentación 225 | 226 | - [Referencia de Documentación](https://docs.culqi.com/) 227 | - [Referencia de API](https://apidocs.culqi.com/) 228 | - [Demo Checkout V4 + Culqi 3DS](https://github.com/culqi/culqi-python-demo-checkoutv4-culqi3ds) 229 | - [Wiki](https://github.com/culqi/culqi-python/wiki) 230 | 231 | ## Changelog 232 | 233 | Todos los cambios en las versiones de esta biblioteca están listados en 234 | [CHANGELOG.md](CHANGELOG.md). 235 | 236 | ## Autor 237 | Team Culqi 238 | 239 | ## Licencia 240 | El código fuente de culqi-python está distribuido bajo MIT License, revisar el archivo LICENSE. 241 | 242 | -------------------------------------------------------------------------------- /culqi/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import VERSION 2 | 3 | __version__ = ".".join(VERSION) 4 | -------------------------------------------------------------------------------- /culqi/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from copy import deepcopy 4 | from types import ModuleType 5 | 6 | from requests import session 7 | 8 | from culqi.utils.constants import CONSTANTS 9 | from culqi.utils.errors import CustomException 10 | from culqi.utils.validation.helpers import Helpers 11 | 12 | from . import resources 13 | from culqi.utils import capitalize_camel_case 14 | from culqi.version import VERSION 15 | import culqi.utils.encoder as encoder 16 | 17 | RESOURCE_CLASSES = {} 18 | SCHEMAS = {} 19 | 20 | for name, module in resources.__dict__.items(): 21 | capitalized_name = capitalize_camel_case(name) 22 | is_module = isinstance(module, ModuleType) 23 | is_in_module = capitalized_name in getattr(module, "__dict__", {}) 24 | if is_module and is_in_module: 25 | RESOURCE_CLASSES[name] = module.__dict__[capitalized_name] 26 | 27 | logger = logging.getLogger() 28 | logger.setLevel(logging.INFO) 29 | 30 | try: 31 | rsa_aes_encoder = encoder.RsaAesEncoder() 32 | except Exception as e: 33 | logger.error('Unable to create an instance of Base64Decoder class.', exc_info=True) 34 | 35 | class Culqi: 36 | def __init__(self, public_key, private_key): 37 | self.public_key = public_key 38 | self.private_key = private_key 39 | self.session = session() 40 | 41 | self._set_client_headers() 42 | 43 | for name, klass in RESOURCE_CLASSES.items(): 44 | setattr(self, name, klass(self)) 45 | 46 | @staticmethod 47 | def _get_version(): 48 | return ".".join(VERSION) 49 | 50 | @staticmethod 51 | def _update_request(data, options): 52 | if data is None: 53 | data = {} 54 | 55 | """Update The resource data and header options.""" 56 | #data = json.dumps(data) 57 | 58 | if "headers" not in options: 59 | options["headers"] = {} 60 | 61 | options["headers"].update( 62 | {"Content-Type": "application/json", "Accept": "application/json"} 63 | ) 64 | 65 | return data, options 66 | 67 | 68 | def _get_client_headers(self): 69 | if 'test' in self.private_key: 70 | xCulqiEnv = CONSTANTS.X_CULQI_ENV_TEST 71 | else: 72 | xCulqiEnv = CONSTANTS.X_CULQI_ENV_LIVE 73 | 74 | return { 75 | "User-Agent": "Culqi-API-Python/{0}".format(self._get_version()), 76 | "Authorization": "Bearer {0}".format(self.private_key), 77 | "Content-Type": "application/json", 78 | "Accept": "application/json", 79 | "x-culqi-env": xCulqiEnv, 80 | "x-api-version": CONSTANTS.X_API_VERSION, 81 | "x-culqi-client": CONSTANTS.X_CULQI_CLIENT, 82 | "x-culqi-client-version": CONSTANTS.X_CULQI_CLIENT_VERSION, 83 | } 84 | 85 | def _set_client_headers(self): 86 | self.session.headers.update( 87 | **self._get_client_headers() 88 | ) 89 | 90 | def _update_client_headers(self, headers): 91 | self.session.headers.update( 92 | { 93 | **self._get_client_headers(), 94 | **headers, 95 | } 96 | ) 97 | 98 | def request(self, method, url, data, **options): 99 | if 'headers' not in options: 100 | options['headers'] = {} 101 | 102 | # Agregar headers únicos a options['headers'] 103 | for key, value in self.session.headers.items(): 104 | if key not in options['headers']: 105 | options['headers'][key] = value 106 | 107 | try: 108 | Helpers.validate_string_start(self.public_key, "pk") 109 | Helpers.validate_string_start(self.private_key, "sk") 110 | except CustomException as e: 111 | return e.error_data 112 | """Dispatch a request to the CULQUI HTTP API.""" 113 | if method == "get": 114 | response = getattr(self.session, method)(url, params=data, **options) 115 | elif method == "delete": 116 | response = getattr(self.session, method)(url, **options) 117 | else: 118 | data = json.dumps(data) 119 | response = getattr(self.session, method)(url, data, **options) 120 | 121 | data = response.json() 122 | if "data" in data: 123 | data["items"] = deepcopy(data["data"]) 124 | del data["data"] 125 | 126 | return {"status": response.status_code, "data": data} 127 | 128 | def get(self, url, params, **options): 129 | data, options = self._update_request(params, options) 130 | return self.request("get", url, data=data, **options) 131 | 132 | def post(self, url, data, **options): 133 | data, options = rsa_aes_encoder.encrypt_validation(data, options) 134 | data, options = self._update_request(data, options) 135 | return self.request("post", url, data, **options) 136 | 137 | def patch(self, url, data, **options): 138 | data, options = rsa_aes_encoder.encrypt_validation(data, options) 139 | data, options = self._update_request(data, options) 140 | return self.request("patch", url, data=data, **options) 141 | 142 | def delete(self, url, data, **options): 143 | data, options = self._update_request(data, options) 144 | return self.request("delete", url, data, **options) 145 | 146 | def put(self, url, data, **options): 147 | data, options = self._update_request(data, options) 148 | return self.request("put", url, data=data, **options) 149 | -------------------------------------------------------------------------------- /culqi/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from culqi.resources.card import Card 2 | from culqi.resources.charge import Charge 3 | from culqi.resources.customer import Customer 4 | from culqi.resources.event import Event 5 | from culqi.resources.iin import Iin 6 | from culqi.resources.order import Order 7 | from culqi.resources.plan import Plan 8 | from culqi.resources.refund import Refund 9 | from culqi.resources.subscription import Subscription 10 | from culqi.resources.token import Token 11 | from culqi.resources.transfer import Transfer 12 | 13 | __all__ = [ 14 | "Card", 15 | "Charge", 16 | "Customer", 17 | "Event", 18 | "Iin", 19 | "Order", 20 | "Plan", 21 | "Refund", 22 | "Subscription", 23 | "Token", 24 | "Transfer", 25 | ] 26 | -------------------------------------------------------------------------------- /culqi/resources/base.py: -------------------------------------------------------------------------------- 1 | from requests.compat import urljoin 2 | from jsonschema import validate 3 | from culqi.utils.constants import CONSTANTS 4 | 5 | from culqi.utils.urls import URL 6 | 7 | __all__ = ["Resource"] 8 | 9 | 10 | class Resource: 11 | endpoint = None 12 | 13 | def __init__(self, client=None): 14 | self.client = client 15 | 16 | def _get(self, url, data, **kwargs): 17 | return self.client.get(url, data, **kwargs) 18 | 19 | def _patch(self, url, data, **kwargs): 20 | return self.client.patch(url, data, **kwargs) 21 | 22 | def _post(self, url, data, **kwargs): 23 | return self.client.post(url, data, **kwargs) 24 | 25 | def _put(self, url, data, **kwargs): 26 | return self.client.put(url, data, **kwargs) 27 | 28 | def _delete(self, url, data, **kwargs): 29 | return self.client.delete(url, data, **kwargs) 30 | 31 | def _get_url(self, *args): 32 | return urljoin( 33 | URL.BASE, 34 | "/".join([URL.VERSION, self.endpoint] + [str(arg) for arg in args]), 35 | ) 36 | def _get_url_secure(self, *args): 37 | return urljoin( 38 | URL.BASE_SECURE, 39 | "/".join([URL.VERSION, self.endpoint] + [str(arg) for arg in args]), 40 | ) 41 | 42 | def _encrypt(self, data, public_key): 43 | return self.client.encrypt(data, public_key) 44 | 45 | def create(self, data, **options): 46 | if (hasattr(self, 'schema')): 47 | validate(instance=data, schema=self.schema) 48 | 49 | for key, value in options.copy().items(): 50 | if key == 'custom_headers': 51 | self.client._update_client_headers({k: v for k, v in value.items() if v not in [False, '', None]}) 52 | del options['custom_headers'] 53 | 54 | url = self._get_url() 55 | return self._post(url, data, **options) 56 | 57 | def list(self, data=None, **options): 58 | url = self._get_url() 59 | return self._get(url, data, **options) 60 | 61 | def read(self, id_, data=None, **options): 62 | url = self._get_url(id_) 63 | return self._get(url, data, **options) 64 | 65 | def update(self, id_, data=None, **options): 66 | url = self._get_url(id_) 67 | return self._patch(url, data, **options) 68 | 69 | def delete(self, id_, data=None, **options): 70 | url = self._get_url(id_) 71 | return self._delete(url, data, **options) 72 | -------------------------------------------------------------------------------- /culqi/resources/card.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import CustomException 2 | from culqi.utils.validation.card_validation import CardValidation 3 | from culqi.utils.urls import URL 4 | from culqi.resources.base import Resource 5 | 6 | __all__ = ["Card"] 7 | 8 | 9 | class Card(Resource): 10 | endpoint = URL.CARD 11 | 12 | def create(self, data, **options): 13 | try: 14 | CardValidation.create(self, data) 15 | return Resource.create(self, data, **options) 16 | except CustomException as e: 17 | return e.error_data 18 | 19 | def list(self, data={}, **options): 20 | try: 21 | CardValidation.list(self, data) 22 | url = self._get_url() 23 | return self._get(url, data, **options) 24 | except CustomException as e: 25 | return e.error_data 26 | 27 | def read(self, id_, data=None, **options): 28 | try: 29 | CardValidation.retrieve(self, id_) 30 | url = self._get_url(id_) 31 | return self._get(url, data, **options) 32 | except CustomException as e: 33 | return e.error_data 34 | 35 | def update(self, id_, data=None, **options): 36 | try: 37 | CardValidation.update(self, id_) 38 | url = self._get_url(id_) 39 | return self._patch(url, data, **options) 40 | except CustomException as e: 41 | return e.error_data 42 | 43 | def delete(self, id_, data=None, **options): 44 | try: 45 | CardValidation.retrieve(self, id_) 46 | url = self._get_url(id_) 47 | return self._delete(url, data, **options) 48 | except CustomException as e: 49 | return e.error_data 50 | -------------------------------------------------------------------------------- /culqi/resources/charge.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.validation.charge_validation import ChargeValidation 2 | from culqi.utils.errors import CustomException, ErrorMessage, NotAllowedError 3 | from culqi.utils.urls import URL 4 | from culqi.resources.base import Resource 5 | 6 | __all__ = ["Charge"] 7 | 8 | 9 | class Charge(Resource): 10 | endpoint = URL.CHARGE 11 | 12 | def create(self, data, **options): 13 | try: 14 | ChargeValidation.create(self, data) 15 | return Resource.create(self, data, **options) 16 | except CustomException as e: 17 | return e.error_data 18 | 19 | def delete(self, id_, data=None, **options): 20 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 21 | 22 | def capture(self, id_, data=None, **options): 23 | try: 24 | ChargeValidation.capture(self, id_) 25 | url = self._get_url(id_, "capture") 26 | return self._post(url, data, **options) 27 | except CustomException as e: 28 | return e.error_data 29 | 30 | def list(self, data={}, **options): 31 | try: 32 | ChargeValidation.list(self, data) 33 | url = self._get_url() 34 | return self._get(url, data, **options) 35 | except CustomException as e: 36 | return e.error_data 37 | 38 | def read(self, id_, data=None, **options): 39 | try: 40 | ChargeValidation.retrieve(self, id_) 41 | url = self._get_url(id_) 42 | return self._get(url, data, **options) 43 | except CustomException as e: 44 | return e.error_data 45 | 46 | def update(self, id_, data=None, **options): 47 | try: 48 | ChargeValidation.update(self, id_) 49 | url = self._get_url(id_) 50 | return self._patch(url, data, **options) 51 | except CustomException as e: 52 | return e.error_data 53 | -------------------------------------------------------------------------------- /culqi/resources/customer.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import CustomException 2 | from culqi.utils.validation.customer_validation import CustomerValidation 3 | from culqi.utils.urls import URL 4 | from culqi.resources.base import Resource 5 | 6 | __all__ = ["Customer"] 7 | 8 | 9 | class Customer(Resource): 10 | endpoint = URL.CUSTOMER 11 | 12 | def create(self, data, **options): 13 | try: 14 | CustomerValidation.create(self, data) 15 | return Resource.create(self, data, **options) 16 | except CustomException as e: 17 | return e.error_data 18 | 19 | def list(self, data={}, **options): 20 | try: 21 | CustomerValidation.list(self, data) 22 | url = self._get_url() 23 | return self._get(url, data, **options) 24 | except CustomException as e: 25 | return e.error_data 26 | 27 | def read(self, id_, data=None, **options): 28 | try: 29 | CustomerValidation.retrieve(self, id_) 30 | url = self._get_url(id_) 31 | return self._get(url, data, **options) 32 | except CustomException as e: 33 | return e.error_data 34 | 35 | def update(self, id_, data=None, **options): 36 | try: 37 | CustomerValidation.update(self, id_) 38 | url = self._get_url(id_) 39 | return self._patch(url, data, **options) 40 | except CustomException as e: 41 | return e.error_data 42 | 43 | def delete(self, id_, data=None, **options): 44 | try: 45 | CustomerValidation.retrieve(self, id_) 46 | url = self._get_url(id_) 47 | return self._delete(url, data, **options) 48 | except CustomException as e: 49 | return e.error_data 50 | -------------------------------------------------------------------------------- /culqi/resources/event.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import ErrorMessage, NotAllowedError 2 | from culqi.utils.urls import URL 3 | from culqi.resources.base import Resource 4 | 5 | __all__ = ["Event"] 6 | 7 | 8 | class Event(Resource): 9 | endpoint = URL.EVENT 10 | 11 | def create(self, data, **options): 12 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 13 | 14 | def update(self, id_, data=None, **options): 15 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 16 | 17 | def delete(self, id_, data=None, **options): 18 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 19 | -------------------------------------------------------------------------------- /culqi/resources/iin.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import ErrorMessage, NotAllowedError 2 | from culqi.utils.urls import URL 3 | from culqi.resources.base import Resource 4 | 5 | __all__ = ["Iin"] 6 | 7 | 8 | class Iin(Resource): 9 | endpoint = URL.IIN 10 | 11 | def create(self, data, **options): 12 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 13 | 14 | def update(self, id_, data=None, **options): 15 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 16 | 17 | def delete(self, id_, data=None, **options): 18 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 19 | -------------------------------------------------------------------------------- /culqi/resources/order.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import CustomException 2 | from culqi.utils.validation.order_validation import OrderValidation 3 | from culqi.utils.urls import URL 4 | from culqi.resources.base import Resource 5 | 6 | 7 | __all__ = ["Order"] 8 | 9 | 10 | class Order(Resource): 11 | endpoint = URL.ORDER 12 | 13 | def create(self, data, **options): 14 | try: 15 | OrderValidation.create(self, data) 16 | return Resource.create(self, data, **options) 17 | except CustomException as e: 18 | return e.error_data 19 | 20 | def confirm(self, id_, data={}, **options): 21 | try: 22 | OrderValidation.confirm(self, id_) 23 | headers = {"Authorization": "Bearer {0}".format(self.client.public_key)} 24 | if "headers" in options: 25 | options["headers"].update(headers) 26 | else: 27 | options["headers"] = headers 28 | url = self._get_url(id_, "confirm") 29 | return self._post(url, data, **options) 30 | except CustomException as e: 31 | return e.error_data 32 | def confirmtype(self, data={}, **options): 33 | try: 34 | OrderValidation.confirm_type(self, data) 35 | headers = {"Authorization": "Bearer {0}".format(self.client.public_key)} 36 | if "headers" in options: 37 | options["headers"].update(headers) 38 | else: 39 | options["headers"] = headers 40 | url = self._get_url("confirm") 41 | return self._post(url, data, **options) 42 | except CustomException as e: 43 | return e.error_data 44 | 45 | def list(self, data={}, **options): 46 | try: 47 | OrderValidation.list(self, data) 48 | url = self._get_url() 49 | return self._get(url, data, **options) 50 | except CustomException as e: 51 | return e.error_data 52 | 53 | def read(self, id_, data=None, **options): 54 | try: 55 | OrderValidation.retrieve(self, id_) 56 | url = self._get_url(id_) 57 | return self._get(url, data, **options) 58 | except CustomException as e: 59 | return e.error_data 60 | 61 | def update(self, id_, data=None, **options): 62 | try: 63 | OrderValidation.update(self, id_) 64 | url = self._get_url(id_) 65 | return self._patch(url, data, **options) 66 | except CustomException as e: 67 | return e.error_data 68 | 69 | def delete(self, id_, data=None, **options): 70 | try: 71 | OrderValidation.retrieve(self, id_) 72 | url = self._get_url(id_) 73 | return self._delete(url, data, **options) 74 | except CustomException as e: 75 | return e.error_data -------------------------------------------------------------------------------- /culqi/resources/plan.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import CustomException 2 | from culqi.utils.urls import URL 3 | from culqi.resources.base import Resource 4 | from culqi.utils.validation.plan_validation import PlanValidation 5 | from jsonschema import validate 6 | 7 | __all__ = ["Plan"] 8 | 9 | 10 | class Plan(Resource): 11 | endpoint = URL.PLAN 12 | 13 | def create(self, data, **options): 14 | try: 15 | PlanValidation.create(self, data) 16 | if (hasattr(self, 'schema')): 17 | validate(instance=data, schema=self.schema) 18 | url = self._get_url("create") 19 | 20 | return self._post(url, data, **options) 21 | except CustomException as e: 22 | return e.error_data 23 | 24 | def list(self, data={}, **options): 25 | try: 26 | PlanValidation.list(self, data) 27 | url = self._get_url() 28 | return self._get(url, data, **options) 29 | except CustomException as e: 30 | return e.error_data 31 | 32 | def read(self, id_, data=None, **options): 33 | try: 34 | PlanValidation.retrieve(self, id_) 35 | url = self._get_url(id_) 36 | return self._get(url, data, **options) 37 | except CustomException as e: 38 | return e.error_data 39 | 40 | def update(self, id_, data=None, **options): 41 | try: 42 | PlanValidation.update(self, id_, data) 43 | url = self._get_url(id_) 44 | return self._patch(url, data, **options) 45 | except CustomException as e: 46 | return e.error_data 47 | 48 | def delete(self, id_, data=None, **options): 49 | try: 50 | PlanValidation.retrieve(self, id_) 51 | url = self._get_url(id_) 52 | return self._delete(url, data, **options) 53 | except CustomException as e: 54 | return e.error_data 55 | -------------------------------------------------------------------------------- /culqi/resources/refund.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import CustomException, ErrorMessage, NotAllowedError 2 | from culqi.utils.validation.refund_validation import RefundValidation 3 | from culqi.utils.urls import URL 4 | from culqi.resources.base import Resource 5 | 6 | __all__ = ["Refund"] 7 | 8 | 9 | class Refund(Resource): 10 | endpoint = URL.REFUND 11 | 12 | def create(self, data, **options): 13 | try: 14 | RefundValidation.create(self, data) 15 | return Resource.create(self, data, **options) 16 | except CustomException as e: 17 | return e.error_data 18 | 19 | def delete(self, id_, data=None, **options): 20 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 21 | 22 | def list(self, data={}, **options): 23 | try: 24 | RefundValidation.list(self, data) 25 | url = self._get_url() 26 | return self._get(url, data, **options) 27 | except CustomException as e: 28 | return e.error_data 29 | 30 | def read(self, id_, data=None, **options): 31 | try: 32 | RefundValidation.retrieve(self, id_) 33 | url = self._get_url(id_) 34 | return self._get(url, data, **options) 35 | except CustomException as e: 36 | return e.error_data 37 | 38 | def update(self, id_, data=None, **options): 39 | try: 40 | RefundValidation.update(self, id_) 41 | url = self._get_url(id_) 42 | return self._patch(url, data, **options) 43 | except CustomException as e: 44 | return e.error_data 45 | -------------------------------------------------------------------------------- /culqi/resources/subscription.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import CustomException 2 | from culqi.utils.validation.subscription_validation import SubscriptionValidation 3 | from culqi.utils.urls import URL 4 | from culqi.resources.base import Resource 5 | from jsonschema import validate 6 | 7 | __all__ = ["Subscription"] 8 | 9 | 10 | class Subscription(Resource): 11 | endpoint = URL.SUBSCRIPTION 12 | 13 | def create(self, data, **options): 14 | try: 15 | SubscriptionValidation.create(self, data) 16 | if (hasattr(self, 'schema')): 17 | validate(instance=data, schema=self.schema) 18 | url = self._get_url("create") 19 | 20 | return self._post(url, data, **options) 21 | except CustomException as e: 22 | return e.error_data 23 | 24 | def list(self, data={}, **options): 25 | try: 26 | SubscriptionValidation.list(self, data) 27 | url = self._get_url() 28 | return self._get(url, data, **options) 29 | except CustomException as e: 30 | return e.error_data 31 | 32 | def read(self, id_, data={}, **options): 33 | try: 34 | SubscriptionValidation.retrieve(self, id_) 35 | url = self._get_url(id_) 36 | return self._get(url, data, **options) 37 | except CustomException as e: 38 | return e.error_data 39 | 40 | def update(self, id_, data={}, **options): 41 | try: 42 | SubscriptionValidation.update(self, id_, data) 43 | url = self._get_url(id_) 44 | return self._patch(url, data, **options) 45 | except CustomException as e: 46 | return e.error_data 47 | 48 | def delete(self, id_, data=None, **options): 49 | try: 50 | SubscriptionValidation.retrieve(self, id_) 51 | url = self._get_url(id_) 52 | return self._delete(url, data, **options) 53 | except CustomException as e: 54 | return e.error_data 55 | -------------------------------------------------------------------------------- /culqi/resources/token.py: -------------------------------------------------------------------------------- 1 | from ..utils.errors import CustomException, ErrorMessage, NotAllowedError 2 | from ..utils.urls import URL 3 | from .base import Resource 4 | from ..utils.validation.token_validation import TokenValidation 5 | 6 | __all__ = ["Token"] 7 | 8 | 9 | class Token(Resource): 10 | endpoint = URL.TOKEN 11 | 12 | def create(self, data, **options): 13 | try: 14 | TokenValidation.create_token_validation(self, data) 15 | headers = {"Authorization": "Bearer {0}".format(self.client.public_key)} 16 | if "headers" in options: 17 | options["headers"].update(headers) 18 | else: 19 | options["headers"] = headers 20 | url = self._get_url_secure() 21 | return self._post(url, data, **options) 22 | except CustomException as e: 23 | return e.error_data 24 | 25 | def createyape(self, data, **options): 26 | try: 27 | TokenValidation.create_token_yape_validation(self, data) 28 | headers = {"Authorization": "Bearer {0}".format(self.client.public_key)} 29 | if "headers" in options: 30 | options["headers"].update(headers) 31 | else: 32 | options["headers"] = headers 33 | url = self._get_url_secure("yape") 34 | return self._post(url, data, **options) 35 | except CustomException as e: 36 | return e.error_data 37 | 38 | def delete(self, id_, data=None, **options): 39 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 40 | 41 | def read(self, id_, data=None, **options): 42 | try: 43 | TokenValidation.token_retrieve_validation(self, id_) 44 | url = self._get_url(id_) 45 | return self._get(url, data, **options) 46 | except CustomException as e: 47 | return e.error_data 48 | 49 | 50 | def update(self, id_, data=None, **options): 51 | try: 52 | TokenValidation.token_update_validation(self, id_) 53 | url = self._get_url(id_) 54 | return self._patch(url, data, **options) 55 | except CustomException as e: 56 | return e.error_data 57 | 58 | def list(self, data={}, **options): 59 | try: 60 | url = self._get_url() 61 | TokenValidation.token_list_validation(self, data) 62 | return self._get(url, data, **options) 63 | except CustomException as e: 64 | return e.error_data 65 | 66 | -------------------------------------------------------------------------------- /culqi/resources/transfer.py: -------------------------------------------------------------------------------- 1 | from culqi.utils.errors import ErrorMessage, NotAllowedError 2 | from culqi.utils.urls import URL 3 | from culqi.resources.base import Resource 4 | 5 | __all__ = ["Transfer"] 6 | 7 | 8 | class Transfer(Resource): 9 | endpoint = URL.TRANSFER 10 | 11 | def create(self, data, **options): 12 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 13 | 14 | def update(self, id_, data=None, **options): 15 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 16 | 17 | def delete(self, id_, data=None, **options): 18 | raise NotAllowedError(ErrorMessage.NOT_ALLOWED) 19 | -------------------------------------------------------------------------------- /culqi/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import ( 2 | BadRequestError, 3 | ErrorCode, 4 | ErrorMessage, 5 | GatewayError, 6 | HTTPErrorCode, 7 | NotAllowedError, 8 | ServerError, 9 | ) 10 | from .status_codes import HTTPStatusCode 11 | from .urls import URL 12 | 13 | 14 | def capitalize_camel_case(string): 15 | return "".join([item.capitalize() for item in string.split("_")]) 16 | -------------------------------------------------------------------------------- /culqi/utils/constants.py: -------------------------------------------------------------------------------- 1 | class CONSTANTS: 2 | X_CULQI_ENV_TEST = "test" 3 | X_CULQI_ENV_LIVE = "live" 4 | X_API_VERSION = "2" 5 | X_CULQI_CLIENT = "culqi-python" 6 | X_CULQI_CLIENT_VERSION = "1.0.2" -------------------------------------------------------------------------------- /culqi/utils/encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Crypto.PublicKey import RSA 3 | from Crypto.Cipher import PKCS1_OAEP 4 | from Crypto.Cipher import AES 5 | from Crypto.Random import get_random_bytes 6 | from Crypto.Hash import SHA256 7 | import base64 8 | 9 | class RsaAesEncoder: 10 | 11 | def _encrypt(self, data, public_key): 12 | # Generate a random encryption key 13 | key = get_random_bytes(32) 14 | iv = get_random_bytes(16) 15 | 16 | # Message to be encrypted 17 | message = json.dumps(data).encode('utf-8') 18 | 19 | # Initialize the cipher with the key and IV 20 | cipher = AES.new(key, AES.MODE_GCM, iv) 21 | 22 | # Encrypt the message 23 | # Note that the message length must be a multiple of 16 bytes 24 | ciphertext = cipher.encrypt(message) 25 | 26 | encrypted_message = base64.b64encode(ciphertext).decode('utf-8') 27 | 28 | # Encrypt the key with the public key 29 | cipher = PKCS1_OAEP.new(RSA.import_key(public_key), hashAlgo=SHA256) 30 | ciphertext_key = cipher.encrypt(key) 31 | 32 | # Encrypt the iv with the public key 33 | cipher = PKCS1_OAEP.new(RSA.import_key(public_key), hashAlgo=SHA256) 34 | ciphertext_iv = cipher.encrypt(iv) 35 | 36 | # Convert the encrypted message to a string 37 | encrypted_aes_key = base64.b64encode(ciphertext_key).decode() 38 | encrypted_aes_iv = base64.b64encode(ciphertext_iv).decode() 39 | 40 | return { 41 | "encrypted_data": encrypted_message, 42 | "encrypted_key": encrypted_aes_key, 43 | "encrypted_iv": encrypted_aes_iv 44 | } 45 | 46 | def encrypt_validation(self, data, options): 47 | headers = {} 48 | if("rsa_public_key" in options and "rsa_id" in options): 49 | data = self._encrypt(data, options["rsa_public_key"]) 50 | headers["x-culqi-rsa-id"] = options["rsa_id"] 51 | if "headers" in options: 52 | options["headers"].update(headers) 53 | else: 54 | options["headers"] = headers 55 | del options["rsa_public_key"] 56 | del options["rsa_id"] 57 | 58 | return data, options -------------------------------------------------------------------------------- /culqi/utils/errors.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | class ErrorCode: 3 | """Codigos de Denegación de Bancos. 4 | 5 | - EXPIRED_CARD 6 | Tarjeta vencida. La tarjeta está vencida o la fecha de vencimiento 7 | ingresada es incorrecta. 8 | 9 | - STOLEN_CARD 10 | Tarjeta robada. La tarjeta fue bloqueada y reportada al banco emisor 11 | como una tarjeta robada. 12 | 13 | - LOST_CARD 14 | Tarjeta perdida. La tarjeta fue bloqueada y reportada al banco emisor 15 | como una tarjeta perdida. 16 | 17 | - INSUFFICIENT_FUNDS 18 | Fondos insuficientes. La tarjeta no tiene fondos suficientes para 19 | realizar la compra. 20 | 21 | - CONTACT_ISSUER 22 | Contactar emisor. La operación fue denegada por el banco emisor de la 23 | tarjeta y el cliente necesita contactarse con la entidad para conocer 24 | el motivo. 25 | 26 | - INVALID_CVV 27 | CVV inválido. El código de seguridad (CVV2, CVC2, CID) de la tarjeta es 28 | inválido. 29 | 30 | - INCORRECT_CVV 31 | CVV incorrecto. El código de seguridad (CVV2, CVC2, CID) de la tarjeta 32 | es incorrecto. 33 | 34 | - TOO_MANY_ATTEMPTS_CVV 35 | Exceso CVV. El cliente ha intentado demasiadas veces ingresar el código 36 | de seguridad (CVV2, CVC2, CID) de la tarjeta. 37 | 38 | - ISSUER_NOT_AVAILABLE 39 | Emisor no disponible. El banco que emitió la tarjeta no responde. El 40 | cliente debe realizar el pago nuevamente. 41 | 42 | - ISSUER_DECLINE_OPERATION 43 | Operación denegada. La operación fue denegada por el banco emisor de la 44 | tarjeta por una razón desconocida. 45 | 46 | - INVALID_CARD 47 | Tarjeta inválida. La tarjeta utilizada tiene restricciones para este tipo 48 | de compras. El cliente necesita contactarse con el banco emisor para 49 | conocer el motivo de la denegación. 50 | 51 | - PROCESSING_ERROR 52 | Error de procesamiento. Ocurrió un error mientras procesabamos la compra. 53 | Contáctate con culqi.com/soporte para que te demos una solución. 54 | 55 | - FRAUDULENT 56 | Operación fraudulenta. El banco emisor de la tarjeta sospecha que se 57 | trata de una compra fraudulenta. 58 | 59 | - CULQI_CARD 60 | Tarjeta Culqi. Estás utilizando una tarjeta de pruebas de Culqi para 61 | realizar una compra real. 62 | """ 63 | 64 | EXPIRED_CARD = "expired_card" 65 | STOLEN_CARD = "stolen_card" 66 | LOST_CARD = "lost_card" 67 | INSUFFICIENT_FUNDS = "insufficient_funds" 68 | CONTACT_ISSUER = "contact_issuer" 69 | INVALID_CVV = "invalid_cvv" 70 | INCORRECT_CVV = "incorrect_cvv" 71 | TOO_MANY_ATTEMPTS_CVV = "too_many_attempts_cvv" 72 | ISSUER_NOT_AVAILABLE = "issuer_not_available" 73 | ISSUER_DECLINE_OPERATION = "issuer_decline_operation" 74 | INVALID_CARD = "invalid_card" 75 | PROCESSING_ERROR = "processing_error" 76 | FRAUDULENT = "fraudulent" 77 | CULQI_CARD = "culqi_card" 78 | 79 | 80 | class HTTPErrorCode: 81 | """Tipos de Errores. 82 | 83 | - INVALID_REQUEST_ERROR: 84 | HTTP 400 - La petición tiene una sintaxis inválida. 85 | 86 | - AUTHENTICATION_ERROR: 87 | HTTP 401 - La petición no pudo ser procesada debido a problemas con las 88 | llaves. 89 | 90 | - PARAMETER_ERROR: 91 | HTTP 422 - Algún parámetro de cualquier petición es inválido. 92 | 93 | - CARD_ERROR: 94 | HTTP 402 - No se pudo realizar el cargo a una tarjeta. 95 | 96 | - LIMIT_API_ERROR: 97 | HTTP 429 - Estás haciendo muchas peticiones rápidamente al API o 98 | superaste tu límite designado. 99 | 100 | - RESOURCE_ERROR: 101 | HTTP 404 - El recurso no puede ser encontrado, es inválido o tiene un 102 | estado diferente al permitido. 103 | 104 | - API_ERROR: 105 | HTTP 500 y 503 - Engloba cualquier otro tipo de error (Ejemplo: problema 106 | temporal con los servidores de Culqi) y debería de ocurrir muy pocas 107 | veces. 108 | """ 109 | 110 | # HTTP 400 111 | INVALID_REQUEST_ERROR = "invalid_request_error" 112 | # HTTP 401 113 | AUTHENTICATION_ERROR = "authentication_error" 114 | # HTTP 422 115 | PARAMETER_ERROR = "parameter_error" 116 | # HTTP 402 117 | CARD_ERROR = "card_error" 118 | # HTTP 429 119 | LIMIT_API_ERROR = "limit_api_error" 120 | # HTTP 404 121 | RESOURCE_ERROR = "resource_error" 122 | # HTTP 500 503 123 | API_ERROR = "api_error" 124 | 125 | 126 | class ErrorMessage: 127 | NOT_ALLOWED = "You can't perform this action." 128 | 129 | 130 | class BadRequestError(Exception): 131 | pass 132 | 133 | 134 | class GatewayError(Exception): 135 | pass 136 | 137 | 138 | class ServerError(Exception): 139 | pass 140 | 141 | 142 | class NotAllowedError(Exception): 143 | pass 144 | 145 | class CustomException(Exception): 146 | def __init__(self, merchant_message): 147 | self.error_data = { 148 | "status": 400, 149 | "data": { 150 | "object": "error", 151 | "type": "param_error", 152 | "merchant_message": merchant_message, 153 | "user_message": merchant_message 154 | } 155 | } 156 | super().__init__(merchant_message) 157 | -------------------------------------------------------------------------------- /culqi/utils/status_codes.py: -------------------------------------------------------------------------------- 1 | class HTTPStatusCode: 2 | OK = 200 3 | REDIRECT = 300 4 | -------------------------------------------------------------------------------- /culqi/utils/urls.py: -------------------------------------------------------------------------------- 1 | class URL: 2 | BASE = "https://api.culqi.com" 3 | BASE_SECURE = "https://secure.culqi.com" 4 | VERSION = "v2" 5 | 6 | CARD = "cards" 7 | CHARGE = "charges" 8 | CUSTOMER = "customers" 9 | EVENT = "events" 10 | ORDER = "orders" 11 | PLAN = "recurrent/plans" 12 | REFUND = "refunds" 13 | SUBSCRIPTION = "recurrent/subscriptions" 14 | TOKEN = "tokens" 15 | 16 | TRANSFER = "transfers" 17 | IIN = "iins" 18 | -------------------------------------------------------------------------------- /culqi/utils/validation/card_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class CardValidation: 9 | 10 | def create(self, data): 11 | # Validate customer and token format 12 | Helpers.validate_string_start(data['customer_id'], "cus") 13 | Helpers.validate_string_start(data['token_id'], "tkn") 14 | 15 | def retrieve(self, id): 16 | Helpers.validate_string_start(id, "crd") 17 | 18 | def update(self, id): 19 | Helpers.validate_string_start(id, "crd") 20 | 21 | def list(self, data): 22 | if 'card_brand' in data: 23 | allowed_brand_values = ['Visa', 'Mastercard', 'Amex', 'Diners'] 24 | Helpers.validate_value(data['card_brand'], allowed_brand_values) 25 | 26 | if 'card_type' in data: 27 | allowed_card_type_values = ['credito', 'debito', 'internacional'] 28 | Helpers.validate_value(data['card_type'], allowed_card_type_values) 29 | 30 | if 'creation_date_from' in data and 'creation_date_to' in data: 31 | Helpers.validate_date_filter(data['creation_date_from'], data['creation_date_to']) 32 | 33 | if 'country_code' in data: 34 | Helpers.validate_value(data['country_code'], get_country_codes()) -------------------------------------------------------------------------------- /culqi/utils/validation/charge_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class ChargeValidation: 9 | 10 | def create(self, data): 11 | # Validate email 12 | if not Helpers.is_valid_email(data['email']): 13 | raise CustomException('Invalid email.') 14 | 15 | # Validate amount 16 | amount = data['amount'] 17 | if isinstance(amount, str): 18 | try: 19 | amount = int(amount) 20 | except CustomException: 21 | raise CustomException("Invalid 'amount'. It should be an integer or a string representing an integer.") 22 | 23 | if not isinstance(amount, int): 24 | raise CustomException("Invalid 'amount'. It should be an integer or a string representing an integer.") 25 | 26 | Helpers.validate_currency_code(data['currency_code']) 27 | 28 | if data['source_id'].startswith("tkn"): 29 | Helpers.validate_string_start(data['source_id'], "tkn") 30 | elif data['source_id'].startswith("ype"): 31 | Helpers.validate_string_start(data['source_id'], "ype") 32 | elif data['source_id'].startswith("crd"): 33 | Helpers.validate_string_start(data['source_id'], "crd") 34 | else: 35 | raise CustomException(f'Incorrect format. The format must start with tkn, ype or crd') 36 | 37 | def retrieve(self, id): 38 | Helpers.validate_string_start(id, "chr") 39 | 40 | def update(self, id): 41 | Helpers.validate_string_start(id, "chr") 42 | 43 | def capture(self, id): 44 | Helpers.validate_string_start(id, "chr") 45 | 46 | def list(self, data): 47 | # Validate email 48 | if 'email' in data: 49 | if not Helpers.is_valid_email(data['email']): 50 | raise CustomException('Invalid email.') 51 | # Validate amount 52 | if 'amount' in data: 53 | # Validate amount 54 | amount = data['amount'] 55 | if isinstance(amount, str): 56 | try: 57 | amount = int(amount) 58 | except CustomException: 59 | raise CustomException("Invalid 'amount'. It should be an integer or a string representing an integer.") 60 | 61 | if not isinstance(amount, int): 62 | raise CustomException("Invalid 'amount'. It should be an integer or a string representing an integer.") 63 | 64 | if 'min_amount' in data: 65 | if not isinstance(data['min_amount'], (int)) or int(data['min_amount']) != data['min_amount']: 66 | raise CustomException('Invalid min amount.') 67 | if 'max_amount' in data: 68 | if not isinstance(data['max_amount'], (int)) or int(data['max_amount']) != data['max_amount']: 69 | raise CustomException('Invalid max amount.') 70 | if 'installments' in data: 71 | if not isinstance(data['installments'], (int)) or int(data['installments']) != data['installments']: 72 | raise CustomException('Invalid installments.') 73 | if 'min_installments' in data: 74 | if not isinstance(data['min_installments'], (int)) or int(data['min_installments']) != data['min_installments']: 75 | raise CustomException('Invalid min installments.') 76 | if 'max_installments' in data: 77 | if not isinstance(data['max_installments'], (int)) or int(data['max_installments']) != data['max_installments']: 78 | raise CustomException('Invalid max installments.') 79 | 80 | if 'currency_code' in data: 81 | Helpers.validate_currency_code(data['currency_code']) 82 | 83 | if 'card_brand' in data: 84 | allowed_brand_values = ['Visa', 'Mastercard', 'Amex', 'Diners'] 85 | Helpers.validate_value(data['card_brand'], allowed_brand_values) 86 | 87 | if 'card_type' in data: 88 | allowed_card_type_values = ['credito', 'debito', 'internacional'] 89 | Helpers.validate_value(data['card_type'], allowed_card_type_values) 90 | 91 | if 'creation_date_from' in data and 'creation_date_to' in data: 92 | Helpers.validate_date_filter(data['creation_date_from'], data['creation_date_to']) 93 | 94 | if 'country_code' in data: 95 | Helpers.validate_value(data['country_code'], get_country_codes()) -------------------------------------------------------------------------------- /culqi/utils/validation/country_codes.py: -------------------------------------------------------------------------------- 1 | def get_country_codes(): 2 | return ['AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 3 | 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 4 | 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 5 | 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 6 | 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 7 | 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 8 | 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 9 | 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 10 | 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 11 | 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 12 | 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 13 | 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 14 | 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 15 | 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 16 | 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 17 | 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW'] 18 | -------------------------------------------------------------------------------- /culqi/utils/validation/customer_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class CustomerValidation: 9 | 10 | def create(self, data): 11 | # Validate address, firstname, and lastname 12 | if not data.get('first_name'): 13 | raise CustomException('first name is empty.') 14 | 15 | if not data.get('last_name'): 16 | raise CustomException('last name is empty.') 17 | 18 | if not data.get('address'): 19 | raise CustomException('address is empty.') 20 | 21 | if not data.get('address_city'): 22 | raise CustomException('address_city is empty.') 23 | 24 | # Validate country code 25 | Helpers.validate_value(data['country_code'], get_country_codes()) 26 | 27 | # Validate email 28 | if not Helpers.is_valid_email(data['email']): 29 | raise CustomException('Invalid email.') 30 | 31 | def retrieve(self, id): 32 | Helpers.validate_string_start(id, "cus") 33 | 34 | def update(self, id): 35 | Helpers.validate_string_start(id, "cus") 36 | 37 | def list(self, data): 38 | if 'email' in data: 39 | if not Helpers.is_valid_email(data['email']): 40 | raise CustomException('Invalid email.') 41 | 42 | if 'country_code' in data: 43 | Helpers.validate_value(data['country_code'], get_country_codes()) -------------------------------------------------------------------------------- /culqi/utils/validation/helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.errors import CustomException 5 | 6 | class Helpers: 7 | 8 | def is_valid_card_number(number): 9 | return re.match(r'^\d{13,19}$', number) is not None 10 | 11 | def is_valid_email(email): 12 | return re.match(r'^\S+@\S+\.\S+$', email) is not None 13 | 14 | def validate_currency_code(currency_code): 15 | if not currency_code: 16 | raise CustomException('Currency code is empty.') 17 | 18 | if not isinstance(currency_code, str): 19 | raise CustomException('Currency code must be a string.') 20 | 21 | allowed_values = ['PEN', 'USD'] 22 | if currency_code not in allowed_values: 23 | raise CustomException('Currency code must be either "PEN" or "USD".') 24 | 25 | def validate_string_start(string, start): 26 | if not string.startswith(start + "_test_") and not string.startswith(start + "_live_"): 27 | raise CustomException(f'Incorrect format. The format must start with {start}_test_ or {start}_live_') 28 | 29 | def validate_value(value, allowed_values): 30 | if value not in allowed_values: 31 | raise CustomException(f'Invalid value. It must be {json.dumps(allowed_values)}.') 32 | 33 | def is_future_date(expiration_date): 34 | exp_date = datetime.datetime.fromtimestamp(expiration_date) 35 | return exp_date > datetime.datetime.now() 36 | 37 | def validate_date_filter(date_from, date_to): 38 | _date_from = int(date_from) 39 | _date_to = int(date_to) 40 | 41 | if(_date_to < _date_from): 42 | raise CustomException('Invalid value. Date_from it must be less than date_to') 43 | 44 | def additional_validation(data, required_fields): 45 | for field in required_fields: 46 | if field not in data or data[field] is None or data[field] == "" or data[field] == "undefined": 47 | return CustomException(f"El campo '{field}' es requerido.") 48 | 49 | return None 50 | 51 | def validate_initial_cycles_parameters(initial_cycles): 52 | parameters_initial_cycles_response = { 53 | 'count': "El campo 'initial_cycles.count' es inválido o está vacío, debe tener un valor numérico.", 54 | 'has_initial_charge': "El campo 'initial_cycles.has_initial_charge' es inválido o está vacío. El valor debe ser un booleano (true o false).", 55 | 'amount': "El campo 'initial_cycles.amount' es inválido o está vacío, debe tener un valor numérico.", 56 | 'interval_unit_time': "El campo 'initial_cycles.interval_unit_time' tiene un valor inválido o está vacío. Estos son los únicos valores permitidos: [1,2,3,4,5,6]" 57 | } 58 | parameters_initial_cycles = ['count', 'has_initial_charge', 'amount', 'interval_unit_time'] 59 | for campo in parameters_initial_cycles: 60 | if campo not in initial_cycles: 61 | raise CustomException(parameters_initial_cycles_response[campo]) 62 | 63 | if not isinstance(initial_cycles['count'], int): 64 | raise CustomException(parameters_initial_cycles_response['count']) 65 | 66 | if not isinstance(initial_cycles['has_initial_charge'], bool): 67 | raise CustomException(parameters_initial_cycles_response['has_initial_charge']) 68 | 69 | if not isinstance(initial_cycles['amount'], int): 70 | raise CustomException(parameters_initial_cycles_response['amount']) 71 | 72 | valuesIntervalUnitTime = [1, 2, 3, 4, 5, 6] 73 | if not isinstance(initial_cycles['interval_unit_time'], int) or initial_cycles['interval_unit_time'] not in valuesIntervalUnitTime: 74 | raise CustomException(parameters_initial_cycles_response['interval_unit_time']) 75 | 76 | def validate_enum_currency(currency): 77 | allowed_values = ["PEN", "USD"] 78 | if currency in allowed_values: 79 | return None # El valor está en la lista, no hay error 80 | 81 | # Si llega aquí, significa que el valor no está en la lista 82 | raise CustomException(f"El campo 'currency' es inválido o está vacío, el código de la moneda en tres letras (Formato ISO 4217). Culqi actualmente soporta las siguientes monedas: {allowed_values}.") 83 | 84 | 85 | def validate_currency(self, currency, amount): 86 | err = self.validate_enum_currency(currency) 87 | if err is not None: 88 | return CustomException(str(err)) 89 | 90 | min_amount_pen = 3 * 100 91 | max_amount_pen = 5000 * 100 92 | min_amount_usd = 1 * 100 93 | max_amount_usd = 1500 * 100 94 | 95 | min_amount_public_api = min_amount_pen 96 | max_amount_public_api = max_amount_pen 97 | 98 | if currency == "USD": 99 | min_amount_public_api = min_amount_usd 100 | max_amount_public_api = max_amount_usd 101 | 102 | valid_amount = min_amount_public_api <= int(amount) <= max_amount_public_api 103 | 104 | if not valid_amount: 105 | return CustomException(f"El campo 'amount' admite valores en el rango {min_amount_public_api} a {max_amount_public_api}.") 106 | 107 | return None 108 | 109 | def validate_initial_cycles(has_initial_charge, count): 110 | if has_initial_charge: 111 | 112 | if not (1 <= count <= 9999): 113 | raise CustomException("El campo 'initial_cycles.count' solo admite valores numéricos en el rango 1 a 9999.") 114 | else: 115 | if not (0 <= count <= 9999): 116 | raise CustomException("El campo 'initial_cycles.count' solo admite valores numéricos en el rango 0 a 9999.") 117 | 118 | def validate_image(image): 119 | # Expresión regular para validar URLs 120 | regex_image = r'^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-zA-Z0-9]+([-.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?(\/.*)?$' 121 | 122 | # Verificar si 'image' es una cadena y cumple con los criterios de validación 123 | if not (isinstance(image, str) and (5 <= len(image) <= 250) and re.match(regex_image, image)): 124 | # La imagen no cumple con los criterios de validación 125 | raise CustomException("El campo 'image' es inválido. Debe ser una cadena y una URL válida.") 126 | 127 | def validate_metadata(self, metadata): 128 | # Permitir un diccionario vacío para el campo metadata 129 | if not metadata: 130 | return None 131 | 132 | # Verificar límites de longitud de claves y valores 133 | error_length = self.validate_key_and_value_length(metadata) 134 | if error_length is not None: 135 | raise CustomException(error_length) 136 | 137 | # Convertir el diccionario transformado a JSON 138 | try: 139 | json.dumps(metadata) 140 | except json.JSONDecodeError as e: 141 | return e 142 | 143 | return None 144 | 145 | def validate_key_and_value_length(obj_metadata): 146 | max_key_length = 30 147 | max_value_length = 200 148 | 149 | for key, value in obj_metadata.items(): 150 | key_str = str(key) 151 | value_str = str(value) 152 | # Verificar límites de longitud de claves 153 | if not (1 <= len(key_str) <= max_key_length) or not (1 <= len(value_str) <= max_value_length): 154 | return f"El objeto 'metadata' es inválido, límite key (1 - {max_key_length}), value (1 - {max_value_length})." 155 | 156 | return None 157 | -------------------------------------------------------------------------------- /culqi/utils/validation/order_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class OrderValidation: 9 | 10 | def create(self, data): 11 | # Validate amount 12 | if not isinstance(data['amount'], (int, float)) or int(data['amount']) != data['amount']: 13 | raise CustomException('Invalid amount.') 14 | 15 | # Validate currency 16 | Helpers.validate_currency_code(data['currency_code']) 17 | 18 | # Validate firstname, lastname, and phone 19 | client_details = data.get('client_details', {}) 20 | if not client_details.get('first_name'): 21 | raise CustomException('first name is empty.') 22 | 23 | if not client_details.get('last_name'): 24 | raise CustomException('last name is empty.') 25 | 26 | if not client_details.get('phone_number'): 27 | raise CustomException('phone_number is empty.') 28 | 29 | # Validate email 30 | if not Helpers.is_valid_email(client_details.get('email')): 31 | raise CustomException('Invalid email.') 32 | 33 | # Validate expiration date 34 | if not Helpers.is_future_date(data['expiration_date']): 35 | raise CustomException('expiration_date must be a future date.') 36 | 37 | def retrieve(self, id): 38 | Helpers.validate_string_start(id, "ord") 39 | 40 | def update(self, id): 41 | Helpers.validate_string_start(id, "ord") 42 | 43 | def confirm(self, id): 44 | Helpers.validate_string_start(id, "ord") 45 | 46 | def confirm_type(self, data): 47 | Helpers.validate_string_start(data['order_id'], "ord") 48 | 49 | def list(self, data): 50 | if 'amount' in data: 51 | if not isinstance(data['amount'], (int)) or int(data['amount']) != data['amount']: 52 | raise CustomException('Invalid amount.') 53 | if 'min_amount' in data: 54 | if not isinstance(data['min_amount'], (int)) or int(data['min_amount']) != data['min_amount']: 55 | raise CustomException('Invalid min amount.') 56 | if 'max_amount' in data: 57 | if not isinstance(data['max_amount'], (int)) or int(data['max_amount']) != data['max_amount']: 58 | raise CustomException('Invalid max amount.') 59 | 60 | if 'creation_date_from' in data and 'creation_date_to' in data: 61 | Helpers.validate_date_filter(data['creation_date_from'], data['creation_date_to']) 62 | -------------------------------------------------------------------------------- /culqi/utils/validation/plan_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class PlanValidation: 9 | 10 | def create(self, data): 11 | requerid_payload = ['short_name', 'description', 'amount', 'currency', 'interval_unit_time', 12 | 'interval_count', 'initial_cycles', 'name'] 13 | resultValidation = Helpers.additional_validation(data, requerid_payload) 14 | if resultValidation is not None: 15 | raise CustomException(f"{resultValidation}") 16 | else: 17 | 18 | # Validate interval_unit_time 19 | valuesIntervalUnitTime = [1, 2, 3, 4, 5, 6] 20 | if not isinstance(data['interval_unit_time'], int) or data['interval_unit_time'] not in valuesIntervalUnitTime: 21 | raise CustomException("El campo 'interval_unit_time' tiene un valor inválido o está vacío. Estos son los únicos valores permitidos: [ 1, 2, 3, 4, 5, 6]") 22 | 23 | # Validate interval_count 24 | rangeIntervalCount = range(0, 10000) 25 | if not isinstance(data['interval_count'], int) or data['interval_count'] not in rangeIntervalCount: 26 | raise CustomException("El campo 'interval_count' solo admite valores numéricos en el rango 0 a 9999.") 27 | 28 | #Validate amount 29 | if not isinstance(data['amount'], int): 30 | raise CustomException("El campo 'amount' es inválido o está vacío, debe tener un valor numérico.") 31 | 32 | #validateCurrency 33 | Helpers.validate_enum_currency(data['currency']) 34 | 35 | # Validate name 36 | rangeName = range(5, 51) 37 | if not isinstance(data['name'], str) or len(data['name']) not in rangeName: 38 | raise CustomException("El campo 'name' es inválido o está vacío. El valor debe tener un rango de 5 a 50 caracteres.") 39 | 40 | # Validate description 41 | rangeDescription = range(5, 251) 42 | if not isinstance(data['description'], str) or len(data['description']) not in rangeDescription: 43 | raise CustomException("El campo 'description' es inválido o está vacío. El valor debe tener un rango de 5 a 250 caracteres.") 44 | 45 | # Validate short_name 46 | rangeShortName = range(5,51) 47 | if not isinstance(data['short_name'], str) or len(data['short_name']) not in rangeShortName: 48 | raise CustomException("El campo 'short_name' es inválido o está vacío. El valor debe tener un rango de 5 a 50 caracteres.") 49 | 50 | # Validate initial_cycles 51 | Helpers.validate_initial_cycles_parameters(data['initial_cycles']) 52 | initial_cycles = data['initial_cycles'] 53 | Helpers.validate_initial_cycles(initial_cycles['has_initial_charge'], initial_cycles['count']) 54 | 55 | # Validate image 56 | if 'image' in data: 57 | Helpers.validate_image(data['image']) 58 | 59 | # Validate metadata 60 | if 'metadata' in data: 61 | Helpers.validate_metadata(Helpers, data['metadata']) 62 | 63 | def retrieve(self, id): 64 | Helpers.validate_string_start(id, 'pln') 65 | if len(id) != 25: 66 | raise CustomException("El campo 'id' es inválido. La longitud debe ser de 25 caracteres.") 67 | 68 | def update(self, id, data): 69 | Helpers.validate_string_start(id, 'pln') 70 | if len(id) != 25: 71 | raise CustomException("El campo 'id' es inválido. La longitud debe ser de 25 caracteres.") 72 | 73 | # Validate data update 74 | if 'short_name' in data: 75 | rangeShortName = range(5, 51) 76 | if not isinstance(data['short_name'], str) or len(data['short_name']) not in rangeShortName: 77 | raise CustomException("El campo 'short_name' es inválido o está vacío. El valor debe tener un rango de 5 a 50 caracteres.") 78 | 79 | if 'name' in data: 80 | rangeName = range(5, 51) 81 | if not isinstance(data['name'], str) or len(data['name']) not in rangeName: 82 | raise CustomException("El campo 'name' es inválido o está vacío. El valor debe tener un rango de 5 a 50 caracteres.") 83 | 84 | if 'description' in data: 85 | rangeDescription = range(5, 251) 86 | if not isinstance(data['description'], str) or len(data['description']) not in rangeDescription: 87 | raise CustomException("El campo 'description' es inválido o está vacío. El valor debe tener un rango de 5 a 250 caracteres.") 88 | 89 | # Validate image 90 | if 'image' in data: 91 | Helpers.validate_image(data['image']) 92 | 93 | # Validate metadata 94 | if 'metadata' in data: 95 | Helpers.validate_metadata(Helpers, data['metadata']) 96 | 97 | if 'status' in data: 98 | valuesStatus = [1, 2] 99 | if not isinstance(data['status'], int) or data['status'] not in valuesStatus: 100 | raise CustomException("El campo 'status' tiene un valor inválido o está vacío. Estos son los únicos valores permitidos: [ 1, 2 ]") 101 | 102 | def list(self, data): 103 | 104 | # Validate parameters status 105 | if 'status' in data: 106 | valuesStatus = [1, 2] 107 | if not isinstance(data['status'], int) or data['status'] not in valuesStatus: 108 | raise CustomException("El filtro 'status' tiene un valor inválido o está vacío. Estos son los únicos valores permitidos: 1, 2.") 109 | 110 | # Validate parameters creation_date_from 111 | if 'creation_date_from' in data : 112 | if not isinstance(data['creation_date_from'], str) or not(len(data['creation_date_from']) == 10 or len(data['creation_date_from']) == 13) : 113 | raise CustomException("El campo 'creation_date_from' debe tener una longitud de 10 o 13 caracteres.") 114 | 115 | # Validate parameters creation_date_to 116 | if 'creation_date_to' in data : 117 | if not isinstance(data['creation_date_to'], str) or not(len(data['creation_date_to']) == 10 or len(data['creation_date_to']) == 13) : 118 | raise CustomException("El campo 'creation_date_to' debe tener una longitud de 10 o 13 caracteres.") 119 | 120 | # Validate parameters before 121 | if 'before' in data : 122 | if not isinstance(data['before'], str) or len(data['before']) != 25 : 123 | raise CustomException("El campo 'before' es inválido. La longitud debe ser de 25 caracteres") 124 | 125 | # Validate parameters after 126 | if 'after' in data : 127 | if not isinstance(data['after'], str) or len(data['after']) != 25 : 128 | raise CustomException("El campo 'after' es inválido. La longitud debe ser de 25 caracteres") 129 | 130 | # Validate parameters limit 131 | if 'limit' in data : 132 | rangeLimit = range(1, 101) 133 | if not isinstance(data['limit'], int) or data['limit'] not in rangeLimit: 134 | raise CustomException("El filtro 'limit' admite valores en el rango 1 a 100") 135 | 136 | # Validate parameters max_amount 137 | if 'max_amount' in data : 138 | if not isinstance(data['max_amount'], int): 139 | raise CustomException("El filtro 'max_amount' es invalido, debe tener un valor numérico entero.") 140 | 141 | if 'min_amount' in data : 142 | if not isinstance(data['min_amount'], int): 143 | raise CustomException("El filtro 'min_amount' es invalido, debe tener un valor numérico entero.") 144 | 145 | if 'creation_date_from' in data and 'creation_date_to' in data: 146 | Helpers.validate_date_filter(data['creation_date_from'], data['creation_date_to']) -------------------------------------------------------------------------------- /culqi/utils/validation/refund_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class RefundValidation: 9 | 10 | def create(self, data): 11 | # Validate charge format 12 | Helpers.validate_string_start(data['charge_id'], "chr") 13 | 14 | # Validate reason 15 | allowed_values = ['duplicado', 'fraudulento', 'solicitud_comprador'] 16 | Helpers.validate_value(data['reason'], allowed_values) 17 | 18 | # Validate amount 19 | if not isinstance(data['amount'], (int, float)) or int(data['amount']) != data['amount']: 20 | raise CustomException('Invalid amount.') 21 | 22 | def retrieve(self, id): 23 | Helpers.validate_string_start(id, "ref") 24 | 25 | def update(self, id): 26 | Helpers.validate_string_start(id, "ref") 27 | 28 | def list(self, data): 29 | # Validate reason 30 | if 'reason' in data: 31 | allowed_values = ['duplicado', 'fraudulento', 'solicitud_comprador'] 32 | Helpers.validate_value(data['reason'], allowed_values) 33 | 34 | if 'creation_date_from' in data and 'creation_date_to' in data: 35 | Helpers.validate_date_filter(data['creation_date_from'], data['creation_date_to']) -------------------------------------------------------------------------------- /culqi/utils/validation/subscription_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class SubscriptionValidation: 9 | 10 | def create(self, data): 11 | requerid_payload = ['card_id', 'plan_id', 'tyc'] 12 | resultValidation = Helpers.additional_validation(data, requerid_payload) 13 | if resultValidation is not None: 14 | raise CustomException(f"{resultValidation}") 15 | else: 16 | # Validate card_id 17 | if not isinstance(data['card_id'], str) or len(data['card_id'])!= 25: 18 | raise CustomException("El campo 'card_id' es inválido. La longitud debe ser de 25.") 19 | Helpers.validate_string_start(data['card_id'], "crd") 20 | 21 | # Validate plan_id 22 | if not isinstance(data['plan_id'], str) or len(data['plan_id'])!= 25: 23 | raise CustomException("El campo 'plan_id' es inválido. La longitud debe ser de 25.") 24 | Helpers.validate_string_start(data['plan_id'], "pln") 25 | 26 | # Validate tyc 27 | if not isinstance(data['tyc'], bool) : 28 | raise CustomException("El campo 'tyc' es inválido o está vacío. El valor debe ser un booleano.") 29 | 30 | # Validate metadata 31 | if 'metadata' in data: 32 | Helpers.validate_metadata(Helpers, data['metadata']) 33 | 34 | 35 | 36 | def retrieve(self, id): 37 | Helpers.validate_string_start(id, "sxn") 38 | if len(id) != 25: 39 | raise CustomException("El campo 'id' es inválido. La longitud debe ser de 25 caracteres.") 40 | 41 | 42 | def update(self, id, data): 43 | Helpers.validate_string_start(id, "sxn") 44 | if len(id) != 25: 45 | raise CustomException("El campo 'id' es inválido. La longitud debe ser de 25 caracteres.") 46 | # Validate data update 47 | requerid_payload = ['card_id'] 48 | resultValidation = Helpers.additional_validation(data, requerid_payload) 49 | if resultValidation is not None: 50 | raise CustomException(f"{resultValidation}") 51 | else: 52 | # Validate card_id 53 | if not isinstance(data['card_id'], str) or len(data['card_id'])!= 25: 54 | raise CustomException("El campo 'card_id' es inválido. La longitud debe ser de 25.") 55 | Helpers.validate_string_start(data['card_id'], "crd") 56 | 57 | # Validate metadata 58 | if 'metadata' in data: 59 | Helpers.validate_metadata(Helpers, data['metadata']) 60 | 61 | def list(self, data): 62 | # Validate plan_id 63 | if 'plan_id' in data: 64 | if not isinstance(data['plan_id'], str) or len(data['plan_id'])!= 25: 65 | raise CustomException("El campo 'plan_id' es inválido. La longitud debe ser de 25.") 66 | Helpers.validate_string_start(data['plan_id'], "pln") 67 | 68 | if 'status' in data: 69 | valuesStatus = [1, 2, 3, 4, 5, 6, 8] 70 | if not isinstance(data['status'], int) or data['status'] not in valuesStatus: 71 | raise CustomException("El campo 'status' es inválido. Estos son los únicos valores permitidos: 1, 2, 3, 4, 5, 6, 8") 72 | 73 | # Validate parameters creation_date_from 74 | if 'creation_date_from' in data : 75 | if not isinstance(data['creation_date_from'], str) or not(len(data['creation_date_from']) == 10 or len(data['creation_date_from']) == 13) : 76 | raise CustomException("El campo 'creation_date_from' debe tener una longitud de 10 o 13 caracteres.") 77 | 78 | # Validate parameters creation_date_to 79 | if 'creation_date_to' in data : 80 | if not isinstance(data['creation_date_to'], str) or not(len(data['creation_date_to']) == 10 or len(data['creation_date_to']) == 13) : 81 | raise CustomException("El campo 'creation_date_to' debe tener una longitud de 10 o 13 caracteres.") 82 | 83 | # Validate parameters before 84 | if 'before' in data : 85 | if not isinstance(data['before'], str) or len(data['before']) != 25 : 86 | raise CustomException("El campo 'before' es inválido. La longitud debe ser de 25 caracteres") 87 | 88 | # Validate parameters after 89 | if 'after' in data : 90 | if not isinstance(data['after'], str) or len(data['after']) != 25 : 91 | raise CustomException("El campo 'after' es inválido. La longitud debe ser de 25 caracteres") 92 | 93 | # Validate parameters limit 94 | if 'limit' in data : 95 | rangeLimit = range(1, 101) 96 | if not isinstance(data['limit'], int) or data['limit'] not in rangeLimit: 97 | raise CustomException("El filtro 'limit' admite valores en el rango 1 a 100") 98 | 99 | if 'creation_date_from' in data and 'creation_date_to' in data: 100 | Helpers.validate_date_filter(data['creation_date_from'], data['creation_date_to']) 101 | -------------------------------------------------------------------------------- /culqi/utils/validation/token_validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import datetime 4 | from culqi.utils.validation.country_codes import get_country_codes 5 | from culqi.utils.errors import CustomException 6 | from culqi.utils.validation.helpers import Helpers 7 | 8 | class TokenValidation: 9 | def create_token_validation(self, data): 10 | # Validate card number 11 | if not Helpers.is_valid_card_number(data['card_number']): 12 | raise CustomException('Invalid card number.') 13 | 14 | # Validate CVV 15 | if not re.match(r'^\d{3,4}$', data['cvv']): 16 | raise CustomException('Invalid CVV.') 17 | 18 | # Validate email 19 | if not Helpers.is_valid_email(data['email']): 20 | raise CustomException('Invalid email.') 21 | 22 | # Validate expiration month 23 | if not re.match(r'^(0?[1-9]|1[012])$', str(data['expiration_month'])): 24 | raise CustomException('Invalid expiration month.') 25 | 26 | # Validate expiration year 27 | current_year = datetime.datetime.now().year 28 | if not re.match(r'^\d{4}$', str(data['expiration_year'])) or int(str(data['expiration_year'])) < current_year: 29 | raise CustomException('Invalid expiration year.') 30 | 31 | # Check if the card is expired 32 | exp_date = datetime.datetime.strptime(str(data['expiration_year']) + '-' + str(data['expiration_month']), "%Y-%m") 33 | if exp_date < datetime.datetime.now(): 34 | raise CustomException('Card has expired.') 35 | 36 | def create_token_yape_validation(self, data): 37 | # Validate amount 38 | amount = data['amount'] 39 | if isinstance(amount, str): 40 | try: 41 | amount = int(amount) 42 | except CustomException: 43 | raise CustomException("Invalid 'amount'. It should be an integer or a string representing an integer.") 44 | 45 | if not isinstance(amount, int): 46 | raise CustomException("Invalid 'amount'. It should be an integer or a string representing an integer.") 47 | 48 | def token_retrieve_validation(self, id): 49 | Helpers.validate_string_start(id, "tkn") 50 | 51 | def token_update_validation(self, id): 52 | Helpers.validate_string_start(id, "tkn") 53 | 54 | def token_list_validation(self, data): 55 | if 'device_type' in data: 56 | allowed_device_values = ['escritorio', 'movil', 'tablet'] 57 | Helpers.validate_value(data['device_type'], allowed_device_values) 58 | 59 | if 'card_brand' in data: 60 | allowed_brand_values = ['Visa', 'Mastercard', 'Amex', 'Diners'] 61 | Helpers.validate_value(data['card_brand'], allowed_brand_values) 62 | 63 | if 'card_type' in data: 64 | allowed_card_type_values = ['credito', 'debito', 'internacional'] 65 | Helpers.validate_value(data['card_type'], allowed_card_type_values) 66 | 67 | if 'country_code' in data: 68 | Helpers.validate_value(data['country_code'], get_country_codes()) 69 | 70 | if 'creation_date_from' in data and 'creation_date_to' in data: 71 | Helpers.validate_date_filter(data['creation_date_from'], data['creation_date_to']) -------------------------------------------------------------------------------- /culqi/version.py: -------------------------------------------------------------------------------- 1 | VERSION = ("1", "0", "0") 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "culqi" 3 | version = "1.0.0" 4 | description = 'Biblioteca de Culqi en Python' 5 | authors = ["zodiacfireworks "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.7" 9 | requests = "^2.22" 10 | jsonschema = "^3.2.0" 11 | 12 | [tool.poetry.dev-dependencies] 13 | jupyterlab = "^1.2" 14 | twine = "^3.1" 15 | pylint = "^2.4" 16 | autopep8 = "^1.4" 17 | coverage = "^5.0" 18 | pytest-cov = "^2.8" 19 | python-dotenv = "^0.10.3" 20 | mypy = "^0.761.0" 21 | tox = "^3.14" 22 | codecov = "^2.0" 23 | flake8 = "^3.7" 24 | pytest = "^5.3" 25 | pytest-vcr = "^1.0" 26 | black = "19.10b0" 27 | pydocstyle = "^5.0" 28 | pre-commit = "^1.21.0" 29 | flake8-black = "^0.1.1" 30 | 31 | [tool.black] 32 | target_version = ['py35', 'py36', 'py37', 'py38'] 33 | include = '\.pyi?$' 34 | exclude = ''' 35 | /(\.git/ 36 | |\.eggs 37 | |\.hg 38 | |__pycache__ 39 | |\.cache 40 | |\.ipynb_checkpoints 41 | |\.mypy_cache 42 | |\.pytest_cache 43 | |\.tox 44 | |\.venv 45 | |_build 46 | |buck-out 47 | |build 48 | |dist 49 | |legacy 50 | )/ 51 | ''' 52 | [build-system] 53 | requires = ["poetry>=0.12"] 54 | build-backend = "poetry.masonry.api" 55 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 \ 2 | --hash=sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e \ 3 | --hash=sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92 4 | appnope==0.1.0; sys_platform == "darwin" or platform_system == "Darwin" \ 5 | --hash=sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0 \ 6 | --hash=sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71 7 | aspy.yaml==1.3.0 \ 8 | --hash=sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc \ 9 | --hash=sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45 10 | astroid==2.3.3 \ 11 | --hash=sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42 \ 12 | --hash=sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a 13 | atomicwrites==1.3.0; sys_platform == "win32" \ 14 | --hash=sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4 \ 15 | --hash=sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6 16 | attrs==19.3.0 \ 17 | --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ 18 | --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 19 | autopep8==1.4.4 \ 20 | --hash=sha256:4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee 21 | backcall==0.1.0 \ 22 | --hash=sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4 \ 23 | --hash=sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2 24 | black==19.10b0 \ 25 | --hash=sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b \ 26 | --hash=sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539 27 | bleach==3.1.0 \ 28 | --hash=sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16 \ 29 | --hash=sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa 30 | certifi==2019.11.28 \ 31 | --hash=sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3 \ 32 | --hash=sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f 33 | cffi==1.13.2; sys_platform == "linux" \ 34 | --hash=sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43 \ 35 | --hash=sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396 \ 36 | --hash=sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54 \ 37 | --hash=sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159 \ 38 | --hash=sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97 \ 39 | --hash=sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579 \ 40 | --hash=sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc \ 41 | --hash=sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f \ 42 | --hash=sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858 \ 43 | --hash=sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42 \ 44 | --hash=sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b \ 45 | --hash=sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20 \ 46 | --hash=sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3 \ 47 | --hash=sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25 \ 48 | --hash=sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5 \ 49 | --hash=sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c \ 50 | --hash=sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b \ 51 | --hash=sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04 \ 52 | --hash=sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652 \ 53 | --hash=sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57 \ 54 | --hash=sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e \ 55 | --hash=sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d \ 56 | --hash=sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410 \ 57 | --hash=sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a \ 58 | --hash=sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12 \ 59 | --hash=sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e \ 60 | --hash=sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a \ 61 | --hash=sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d \ 62 | --hash=sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3 \ 63 | --hash=sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db \ 64 | --hash=sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506 \ 65 | --hash=sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba \ 66 | --hash=sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346 67 | cfgv==2.0.1 \ 68 | --hash=sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289 \ 69 | --hash=sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144 70 | chardet==3.0.4 \ 71 | --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \ 72 | --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae 73 | click==7.0 \ 74 | --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ 75 | --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 76 | codecov==2.0.15 \ 77 | --hash=sha256:ae00d68e18d8a20e9c3288ba3875ae03db3a8e892115bf9b83ef20507732bed4 \ 78 | --hash=sha256:8ed8b7c6791010d359baed66f84f061bba5bd41174bf324c31311e8737602788 79 | colorama==0.4.3; sys_platform == "win32" or platform_system == "Windows" \ 80 | --hash=sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff \ 81 | --hash=sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1 82 | coverage==5.0.1 \ 83 | --hash=sha256:c90bda74e16bcd03861b09b1d37c0a4158feda5d5a036bb2d6e58de6ff65793e \ 84 | --hash=sha256:bb3d29df5d07d5399d58a394d0ef50adf303ab4fbf66dfd25b9ef258effcb692 \ 85 | --hash=sha256:1ca43dbd739c0fc30b0a3637a003a0d2c7edc1dd618359d58cc1e211742f8bd1 \ 86 | --hash=sha256:591506e088901bdc25620c37aec885e82cc896528f28c57e113751e3471fc314 \ 87 | --hash=sha256:a50b0888d8a021a3342d36a6086501e30de7d840ab68fca44913e97d14487dc1 \ 88 | --hash=sha256:c792d3707a86c01c02607ae74364854220fb3e82735f631cd0a345dea6b4cee5 \ 89 | --hash=sha256:f425f50a6dd807cb9043d15a4fcfba3b5874a54d9587ccbb748899f70dc18c47 \ 90 | --hash=sha256:25b8f60b5c7da71e64c18888f3067d5b6f1334b9681876b2fb41eea26de881ae \ 91 | --hash=sha256:7362a7f829feda10c7265b553455de596b83d1623b3d436b6d3c51c688c57bf6 \ 92 | --hash=sha256:fcd4459fe35a400b8f416bc57906862693c9f88b66dc925e7f2a933e77f6b18b \ 93 | --hash=sha256:40fbfd6b044c9db13aeec1daf5887d322c710d811f944011757526ef6e323fd9 \ 94 | --hash=sha256:7f2675750c50151f806070ec11258edf4c328340916c53bac0adbc465abd6b1e \ 95 | --hash=sha256:24bcfa86fd9ce86b73a8368383c39d919c497a06eebb888b6f0c12f13e920b1a \ 96 | --hash=sha256:eeafb646f374988c22c8e6da5ab9fb81367ecfe81c70c292623373d2a021b1a1 \ 97 | --hash=sha256:2ca2cd5264e84b2cafc73f0045437f70c6378c0d7dbcddc9ee3fe192c1e29e5d \ 98 | --hash=sha256:2cc707fc9aad2592fc686d63ef72dc0031fc98b6fb921d2f5395d9ab84fbc3ef \ 99 | --hash=sha256:04b961862334687549eb91cd5178a6fbe977ad365bddc7c60f2227f2f9880cf4 \ 100 | --hash=sha256:232f0b52a5b978288f0bbc282a6c03fe48cd19a04202df44309919c142b3bb9c \ 101 | --hash=sha256:cfce79ce41cc1a1dc7fc85bb41eeeb32d34a4cf39a645c717c0550287e30ff06 \ 102 | --hash=sha256:46c9c6a1d1190c0b75ec7c0f339088309952b82ae8d67a79ff1319eb4e749b96 \ 103 | --hash=sha256:1cbb88b34187bdb841f2599770b7e6ff8e259dc3bb64fc7893acf44998acf5f8 \ 104 | --hash=sha256:ff3936dd5feaefb4f91c8c1f50a06c588b5dc69fba4f7d9c79a6617ad80bb7df \ 105 | --hash=sha256:65bead1ac8c8930cf92a1ccaedcce19a57298547d5d1db5c9d4d068a0675c38b \ 106 | --hash=sha256:348630edea485f4228233c2f310a598abf8afa5f8c716c02a9698089687b6085 \ 107 | --hash=sha256:960d7f42277391e8b1c0b0ae427a214e1b31a1278de6b73f8807b20c2e913bba \ 108 | --hash=sha256:0101888bd1592a20ccadae081ba10e8b204d20235d18d05c6f7d5e904a38fc10 \ 109 | --hash=sha256:c0fff2733f7c2950f58a4fd09b5db257b00c6fec57bf3f68c5bae004d804b407 \ 110 | --hash=sha256:5f622f19abda4e934938e24f1d67599249abc201844933a6f01aaa8663094489 \ 111 | --hash=sha256:2714160a63da18aed9340c70ed514973971ee7e665e6b336917ff4cca81a25b1 \ 112 | --hash=sha256:b7dbc5e8c39ea3ad3db22715f1b5401cd698a621218680c6daf42c2f9d36e205 \ 113 | --hash=sha256:5ac71bba1e07eab403b082c4428f868c1c9e26a21041436b4905c4c3d4e49b08 114 | cryptography==2.8; sys_platform == "linux" \ 115 | --hash=sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8 \ 116 | --hash=sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2 \ 117 | --hash=sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad \ 118 | --hash=sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2 \ 119 | --hash=sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912 \ 120 | --hash=sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d \ 121 | --hash=sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42 \ 122 | --hash=sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879 \ 123 | --hash=sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d \ 124 | --hash=sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9 \ 125 | --hash=sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c \ 126 | --hash=sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0 \ 127 | --hash=sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf \ 128 | --hash=sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793 \ 129 | --hash=sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595 \ 130 | --hash=sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7 \ 131 | --hash=sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff \ 132 | --hash=sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f \ 133 | --hash=sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e \ 134 | --hash=sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13 \ 135 | --hash=sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651 136 | decorator==4.4.1 \ 137 | --hash=sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d \ 138 | --hash=sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce 139 | defusedxml==0.6.0 \ 140 | --hash=sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93 \ 141 | --hash=sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5 142 | docutils==0.15.2 \ 143 | --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \ 144 | --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \ 145 | --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99 146 | entrypoints==0.3 \ 147 | --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \ 148 | --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 149 | filelock==3.0.12 \ 150 | --hash=sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836 \ 151 | --hash=sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59 152 | flake8==3.7.9 \ 153 | --hash=sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca \ 154 | --hash=sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb 155 | flake8-black==0.1.1 \ 156 | --hash=sha256:56f85aaa5a83f06a3f61e680e3b50f156b5e557ebdcb964d823d86f4c108b0c8 157 | identify==1.4.9 \ 158 | --hash=sha256:72e9c4ed3bc713c7045b762b0d2e2115c572b85abfc1f4604f5a4fd4c6642b71 \ 159 | --hash=sha256:6f44e637caa40d1b4cb37f6ed3b262ede74901d28b1cc5b1fc07360871edd65d 160 | idna==2.8 \ 161 | --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c \ 162 | --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 163 | importlib-metadata==1.3.0; python_version < "3.8" \ 164 | --hash=sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f \ 165 | --hash=sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45 166 | ipykernel==5.1.3 \ 167 | --hash=sha256:1a7def9c986f1ee018c1138d16951932d4c9d4da01dad45f9d34e9899565a22f \ 168 | --hash=sha256:b368ad13edb71fa2db367a01e755a925d7f75ed5e09fbd3f06c85e7a8ef108a8 169 | ipython==7.11.1 \ 170 | --hash=sha256:387686dd7fc9caf29d2fddcf3116c4b07a11d9025701d220c589a430b0171d8a \ 171 | --hash=sha256:0f4bcf18293fb666df8511feec0403bdb7e061a5842ea6e88a3177b0ceb34ead 172 | ipython-genutils==0.2.0 \ 173 | --hash=sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8 \ 174 | --hash=sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8 175 | isort==4.3.21 \ 176 | --hash=sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd \ 177 | --hash=sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1 178 | jedi==0.15.2 \ 179 | --hash=sha256:1349c1e8c107095a55386628bb3b2a79422f3a2cab8381e34ce19909e0cf5064 \ 180 | --hash=sha256:e909527104a903606dd63bea6e8e888833f0ef087057829b89a18364a856f807 181 | jeepney==0.4.2; sys_platform == "linux" \ 182 | --hash=sha256:6f45dce1125cf6c58a1c88123d3831f36a789f9204fbad3172eac15f8ccd08d0 \ 183 | --hash=sha256:0ba6d8c597e9bef1ebd18aaec595f942a264e25c1a48f164d46120eacaa2e9bb 184 | jinja2==2.10.3 \ 185 | --hash=sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f \ 186 | --hash=sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de 187 | json5==0.8.5 \ 188 | --hash=sha256:32bd17e0553bf53927f6c29b6089f3a320c12897120a4bcfea76ea81c10b2d9c \ 189 | --hash=sha256:124b0f0da1ed2ff3bfe3a3e9b8630abd3c650852465cb52c15ef60b8e82a73b0 190 | jsonschema==3.2.0 \ 191 | --hash=sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163 \ 192 | --hash=sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a 193 | jupyter-client==5.3.4 \ 194 | --hash=sha256:d0c077c9aaa4432ad485e7733e4d91e48f87b4f4bab7d283d42bb24cbbba0a0f \ 195 | --hash=sha256:60e6faec1031d63df57f1cc671ed673dced0ed420f4377ea33db37b1c188b910 196 | jupyter-core==4.6.1 \ 197 | --hash=sha256:464769f7387d7a62a2403d067f1ddc616655b7f77f5d810c0dd62cb54bfd0fb9 \ 198 | --hash=sha256:a183e0ec2e8f6adddf62b0a3fc6a2237e3e0056d381e536d3e7c7ecc3067e244 199 | jupyterlab==1.2.4 \ 200 | --hash=sha256:aa8353fed65b3073607ba6a5a806699463ef8f61a7be74f29168440a1567be1e \ 201 | --hash=sha256:6adb88acd05b51512c37df477a18c36240823a591c2a51bf6556198414026d8f 202 | jupyterlab-server==1.0.6 \ 203 | --hash=sha256:d9c3bcf097f7ad8d8fd2f8d0c1e8a1b833671c02808e5f807088975495364447 \ 204 | --hash=sha256:d0977527bfce6f47c782cb6bf79d2c949ebe3f22ac695fa000b730c671445dad 205 | keyring==21.0.0 \ 206 | --hash=sha256:ad84f7fe26ab51731f089eaf1c44ebf4c5fae323653c908888a3a6212ad0bbe7 \ 207 | --hash=sha256:5f5f92327b6c7432bebc18a1b60cb3797d99b08db1f5b919b8187c37a01f1ccc 208 | lazy-object-proxy==1.4.3 \ 209 | --hash=sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0 \ 210 | --hash=sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442 \ 211 | --hash=sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4 \ 212 | --hash=sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a \ 213 | --hash=sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d \ 214 | --hash=sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a \ 215 | --hash=sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e \ 216 | --hash=sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357 \ 217 | --hash=sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50 \ 218 | --hash=sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db \ 219 | --hash=sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449 \ 220 | --hash=sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156 \ 221 | --hash=sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531 \ 222 | --hash=sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb \ 223 | --hash=sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08 \ 224 | --hash=sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383 \ 225 | --hash=sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142 \ 226 | --hash=sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea \ 227 | --hash=sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62 \ 228 | --hash=sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd \ 229 | --hash=sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239 230 | markupsafe==1.1.1 \ 231 | --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ 232 | --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ 233 | --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ 234 | --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ 235 | --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ 236 | --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ 237 | --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ 238 | --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ 239 | --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ 240 | --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ 241 | --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ 242 | --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ 243 | --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ 244 | --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ 245 | --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ 246 | --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ 247 | --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ 248 | --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ 249 | --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ 250 | --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ 251 | --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ 252 | --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ 253 | --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ 254 | --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ 255 | --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ 256 | --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ 257 | --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ 258 | --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b 259 | mccabe==0.6.1 \ 260 | --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ 261 | --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f 262 | mistune==0.8.4 \ 263 | --hash=sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4 \ 264 | --hash=sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e 265 | more-itertools==8.0.2 \ 266 | --hash=sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d \ 267 | --hash=sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564 268 | multidict==4.7.3; python_version >= "3.6" \ 269 | --hash=sha256:ed5f3378c102257df9e2dc9ce6468dabf68bee9ec34969cfdc472631aba00316 \ 270 | --hash=sha256:15a61c0df2d32487e06f6084eabb48fd9e8b848315e397781a70caf9670c9d78 \ 271 | --hash=sha256:d1f45e5bb126662ba66ee579831ce8837b1fd978115c9657e32eb3c75b92973d \ 272 | --hash=sha256:a02fade7b5476c4f88efe9593ff2f3286698d8c6d715ba4f426954f73f382026 \ 273 | --hash=sha256:78bed18e7f1eb21f3d10ff3acde900b4d630098648fe1d65bb4abfb3e22c4900 \ 274 | --hash=sha256:0f04bf4c15d8417401a10a650c349ccc0285943681bfd87d3690587d7714a9b4 \ 275 | --hash=sha256:63ba2be08d82ea2aa8b0f7942a74af4908664d26cb4ff60c58eadb1e33e7da00 \ 276 | --hash=sha256:3c5e2dcbe6b04cbb4303e47a896757a77b676c5e5db5528be7ff92f97ba7ab95 \ 277 | --hash=sha256:c2bfc0db3166e68515bc4a2b9164f4f75ae9c793e9635f8651f2c9ffc65c8dad \ 278 | --hash=sha256:c66d11870ae066499a3541963e6ce18512ca827c2aaeaa2f4e37501cee39ac5d \ 279 | --hash=sha256:aacbde3a8875352a640efa2d1b96e5244a29b0f8df79cbf1ec6470e86fd84697 \ 280 | --hash=sha256:5e5fb8bfebf87f2e210306bf9dd8de2f1af6782b8b78e814060ae9254ab1f297 \ 281 | --hash=sha256:5d2b32b890d9e933d3ced417924261802a857abdee9507b68c75014482145c03 \ 282 | --hash=sha256:cc7f2202b753f880c2e4123f9aacfdb94560ba893e692d24af271dac41f8b8d9 \ 283 | --hash=sha256:bfcad6da0b8839f01a819602aaa5c5a5b4c85ecbfae9b261a31df3d9262fb31e \ 284 | --hash=sha256:73740fcdb38f0adcec85e97db7557615b50ec4e5a3e73e35878720bcee963382 \ 285 | --hash=sha256:be813fb9e5ce41a5a99a29cdb857144a1bd6670883586f995b940a4878dc5238 286 | mypy==0.761 \ 287 | --hash=sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6 \ 288 | --hash=sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36 \ 289 | --hash=sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72 \ 290 | --hash=sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2 \ 291 | --hash=sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0 \ 292 | --hash=sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474 \ 293 | --hash=sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a \ 294 | --hash=sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749 \ 295 | --hash=sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1 \ 296 | --hash=sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7 \ 297 | --hash=sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1 \ 298 | --hash=sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b \ 299 | --hash=sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217 \ 300 | --hash=sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf 301 | mypy-extensions==0.4.3 \ 302 | --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ 303 | --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 304 | nbconvert==5.6.1 \ 305 | --hash=sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee \ 306 | --hash=sha256:21fb48e700b43e82ba0e3142421a659d7739b65568cc832a13976a77be16b523 307 | nbformat==4.4.0 \ 308 | --hash=sha256:b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b \ 309 | --hash=sha256:f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402 310 | nodeenv==1.3.3 \ 311 | --hash=sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a 312 | notebook==6.0.2 \ 313 | --hash=sha256:f67d76a68b1074a91693e95dea903ea01fd02be7c9fac5a4b870b8475caed805 \ 314 | --hash=sha256:399a4411e171170173344761e7fd4491a3625659881f76ce47c50231ed714d9b 315 | packaging==19.2 \ 316 | --hash=sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108 \ 317 | --hash=sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47 318 | pandocfilters==1.4.2 \ 319 | --hash=sha256:b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9 320 | parso==0.5.2 \ 321 | --hash=sha256:5c1f7791de6bd5dbbeac8db0ef5594b36799de198b3f7f7014643b0c5536b9d3 \ 322 | --hash=sha256:55cf25df1a35fd88b878715874d2c4dc1ad3f0eebd1e0266a67e1f55efccfbe1 323 | pathspec==0.7.0 \ 324 | --hash=sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424 \ 325 | --hash=sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96 326 | pexpect==4.7.0; sys_platform != "win32" \ 327 | --hash=sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1 \ 328 | --hash=sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb 329 | pickleshare==0.7.5 \ 330 | --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56 \ 331 | --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca 332 | pkginfo==1.5.0.1 \ 333 | --hash=sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32 \ 334 | --hash=sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb 335 | pluggy==0.13.1 \ 336 | --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d \ 337 | --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 338 | pre-commit==1.21.0 \ 339 | --hash=sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029 \ 340 | --hash=sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850 341 | prometheus-client==0.7.1 \ 342 | --hash=sha256:71cd24a2b3eb335cb800c7159f423df1bd4dcd5171b234be15e3f31ec9f622da 343 | prompt-toolkit==3.0.2 \ 344 | --hash=sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7 \ 345 | --hash=sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990 346 | ptyprocess==0.6.0; sys_platform != "win32" or os_name != "nt" \ 347 | --hash=sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f \ 348 | --hash=sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0 349 | py==1.8.1 \ 350 | --hash=sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0 \ 351 | --hash=sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa 352 | pycodestyle==2.5.0 \ 353 | --hash=sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56 \ 354 | --hash=sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c 355 | pycparser==2.19; sys_platform == "linux" \ 356 | --hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3 357 | pydocstyle==5.0.1 \ 358 | --hash=sha256:4167fe954b8f27ebbbef2fbcf73c6e8ad1e7bb31488fce44a69fdfc4b0cd0fae \ 359 | --hash=sha256:a0de36e549125d0a16a72a8c8c6c9ba267750656e72e466e994c222f1b6e92cb 360 | pyflakes==2.1.1 \ 361 | --hash=sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0 \ 362 | --hash=sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2 363 | pygments==2.5.2 \ 364 | --hash=sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b \ 365 | --hash=sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe 366 | pylint==2.4.4 \ 367 | --hash=sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4 \ 368 | --hash=sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd 369 | pyparsing==2.4.6 \ 370 | --hash=sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec \ 371 | --hash=sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f 372 | pyrsistent==0.15.6 \ 373 | --hash=sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b 374 | pytest==5.3.2 \ 375 | --hash=sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4 \ 376 | --hash=sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa 377 | pytest-cov==2.8.1 \ 378 | --hash=sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b \ 379 | --hash=sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626 380 | pytest-vcr==1.0.2 \ 381 | --hash=sha256:23ee51b75abbcc43d926272773aae4f39f93aceb75ed56852d0bf618f92e1896 \ 382 | --hash=sha256:2f316e0539399bea0296e8b8401145c62b6f85e9066af7e57b6151481b0d6d9c 383 | python-dateutil==2.8.1 \ 384 | --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ 385 | --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a 386 | python-dotenv==0.10.3 \ 387 | --hash=sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544 \ 388 | --hash=sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093 389 | pywin32==227; sys_platform == "win32" \ 390 | --hash=sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0 \ 391 | --hash=sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116 \ 392 | --hash=sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa \ 393 | --hash=sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4 \ 394 | --hash=sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be \ 395 | --hash=sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2 \ 396 | --hash=sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507 \ 397 | --hash=sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511 \ 398 | --hash=sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc \ 399 | --hash=sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e \ 400 | --hash=sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295 \ 401 | --hash=sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c 402 | pywin32-ctypes==0.2.0; sys_platform == "win32" \ 403 | --hash=sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942 \ 404 | --hash=sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98 405 | pywinpty==0.5.7; os_name == "nt" \ 406 | --hash=sha256:b358cb552c0f6baf790de375fab96524a0498c9df83489b8c23f7f08795e966b \ 407 | --hash=sha256:1e525a4de05e72016a7af27836d512db67d06a015aeaf2fa0180f8e6a039b3c2 \ 408 | --hash=sha256:2740eeeb59297593a0d3f762269b01d0285c1b829d6827445fcd348fb47f7e70 \ 409 | --hash=sha256:33df97f79843b2b8b8bc5c7aaf54adec08cc1bae94ee99dfb1a93c7a67704d95 \ 410 | --hash=sha256:e854211df55d107f0edfda8a80b39dfc87015bef52a8fe6594eb379240d81df2 \ 411 | --hash=sha256:dbd838de92de1d4ebf0dce9d4d5e4fc38d0b7b1de837947a18b57a882f219139 \ 412 | --hash=sha256:5fb2c6c6819491b216f78acc2c521b9df21e0f53b9a399d58a5c151a3c4e2a2d \ 413 | --hash=sha256:dd22c8efacf600730abe4a46c1388355ce0d4ab75dc79b15d23a7bd87bf05b48 \ 414 | --hash=sha256:8fc5019ff3efb4f13708bd3b5ad327589c1a554cb516d792527361525a7cb78c \ 415 | --hash=sha256:2d7e9c881638a72ffdca3f5417dd1563b60f603e1b43e5895674c2a1b01f95a0 416 | pyyaml==5.2 \ 417 | --hash=sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc \ 418 | --hash=sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4 \ 419 | --hash=sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15 \ 420 | --hash=sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075 \ 421 | --hash=sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31 \ 422 | --hash=sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc \ 423 | --hash=sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04 \ 424 | --hash=sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd \ 425 | --hash=sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f \ 426 | --hash=sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803 \ 427 | --hash=sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c 428 | pyzmq==18.1.1 \ 429 | --hash=sha256:0573b9790aa26faff33fba40f25763657271d26f64bffb55a957a3d4165d6098 \ 430 | --hash=sha256:972d723a36ab6a60b7806faa5c18aa3c080b7d046c407e816a1d8673989e2485 \ 431 | --hash=sha256:0fa82b9fc3334478be95a5566f35f23109f763d1669bb762e3871a8fa2a4a037 \ 432 | --hash=sha256:80c928d5adcfa12346b08d31360988d843b54b94154575cccd628f1fe91446bc \ 433 | --hash=sha256:efdde21febb9b5d7a8e0b87ea2549d7e00fda1936459cfb27fb6fca0c36af6c1 \ 434 | --hash=sha256:aa3872f2ebfc5f9692ef8957fe69abe92d905a029c0608e45ebfcd451ad30ab5 \ 435 | --hash=sha256:01b588911714a6696283de3904f564c550c9e12e8b4995e173f1011755e01086 \ 436 | --hash=sha256:8ff946b20d13a99dc5c21cb76f4b8b253eeddf3eceab4218df8825b0c65ab23d \ 437 | --hash=sha256:2a294b4f44201bb21acc2c1a17ff87fbe57b82060b10ddb00ac03e57f3d7fcfa \ 438 | --hash=sha256:6fca7d11310430e751f9832257866a122edf9d7b635305c5d8c51f74a5174d3d \ 439 | --hash=sha256:f4e72646bfe79ff3adbf1314906bbd2d67ef9ccc71a3a98b8b2ccbcca0ab7bec \ 440 | --hash=sha256:1e59b7b19396f26e360f41411a5d4603356d18871049cd7790f1a7d18f65fb2c \ 441 | --hash=sha256:cf08435b14684f7f2ca2df32c9df38a79cdc17c20dc461927789216cb43d8363 \ 442 | --hash=sha256:75d73ee7ca4b289a2a2dfe0e6bd8f854979fc13b3fe4ebc19381be3b04e37a4a \ 443 | --hash=sha256:7369656f89878455a5bcd5d56ca961884f5d096268f71c0750fc33d6732a25e5 \ 444 | --hash=sha256:4ec47f2b50bdb97df58f1697470e5c58c3c5109289a623e30baf293481ff0166 \ 445 | --hash=sha256:5541dc8cad3a8486d58bbed076cb113b65b5dd6b91eb94fb3e38a3d1d3022f20 \ 446 | --hash=sha256:ed6205ca0de035f252baa0fd26fdd2bc8a8f633f92f89ca866fd423ff26c6f25 \ 447 | --hash=sha256:8b8498ceee33a7023deb2f3db907ca41d6940321e282297327a9be41e3983792 \ 448 | --hash=sha256:e37f22eb4bfbf69cd462c7000616e03b0cdc1b65f2d99334acad36ea0e4ddf6b \ 449 | --hash=sha256:355b38d7dd6f884b8ee9771f59036bcd178d98539680c4f87e7ceb2c6fd057b6 \ 450 | --hash=sha256:4b73d20aec63933bbda7957e30add233289d86d92a0bb9feb3f4746376f33527 \ 451 | --hash=sha256:d30db4566177a6205ed1badb8dbbac3c043e91b12a2db5ef9171b318c5641b75 \ 452 | --hash=sha256:83ce18b133dc7e6789f64cb994e7376c5aa6b4aeced993048bf1d7f9a0fe6d3a \ 453 | --hash=sha256:d5ac84f38575a601ab20c1878818ffe0d09eb51d6cb8511b636da46d0fd8949a \ 454 | --hash=sha256:e6549dd80de7b23b637f586217a4280facd14ac01e9410a037a13854a6977299 \ 455 | --hash=sha256:a6c9c42bbdba3f9c73aedbb7671815af1943ae8073e532c2b66efb72f39f4165 \ 456 | --hash=sha256:8c69a6cbfa94da29a34f6b16193e7c15f5d3220cb772d6d17425ff3faa063a6d 457 | readme-renderer==24.0 \ 458 | --hash=sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d \ 459 | --hash=sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f 460 | regex==2019.12.20 \ 461 | --hash=sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8 \ 462 | --hash=sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716 \ 463 | --hash=sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9 \ 464 | --hash=sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588 \ 465 | --hash=sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae \ 466 | --hash=sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea \ 467 | --hash=sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e \ 468 | --hash=sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656 \ 469 | --hash=sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1 \ 470 | --hash=sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147 \ 471 | --hash=sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8 \ 472 | --hash=sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d \ 473 | --hash=sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885 \ 474 | --hash=sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b \ 475 | --hash=sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540 \ 476 | --hash=sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63 \ 477 | --hash=sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3 \ 478 | --hash=sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd \ 479 | --hash=sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b \ 480 | --hash=sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f \ 481 | --hash=sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e 482 | requests==2.22.0 \ 483 | --hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31 \ 484 | --hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 485 | requests-toolbelt==0.9.1 \ 486 | --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 \ 487 | --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f 488 | secretstorage==3.1.1; sys_platform == "linux" \ 489 | --hash=sha256:7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a \ 490 | --hash=sha256:20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92 491 | send2trash==1.5.0 \ 492 | --hash=sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b \ 493 | --hash=sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2 494 | six==1.13.0 \ 495 | --hash=sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd \ 496 | --hash=sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66 497 | snowballstemmer==2.0.0 \ 498 | --hash=sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0 \ 499 | --hash=sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52 500 | terminado==0.8.3 \ 501 | --hash=sha256:a43dcb3e353bc680dd0783b1d9c3fc28d529f190bc54ba9a229f72fe6e7a54d7 \ 502 | --hash=sha256:4804a774f802306a7d9af7322193c5390f1da0abb429e082a10ef1d46e6fb2c2 503 | testpath==0.4.4 \ 504 | --hash=sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4 \ 505 | --hash=sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e 506 | toml==0.10.0 \ 507 | --hash=sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3 \ 508 | --hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \ 509 | --hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c 510 | tornado==6.0.3 \ 511 | --hash=sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5 \ 512 | --hash=sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60 \ 513 | --hash=sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281 \ 514 | --hash=sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c \ 515 | --hash=sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5 \ 516 | --hash=sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7 \ 517 | --hash=sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9 518 | tox==3.14.3 \ 519 | --hash=sha256:806d0a9217584558cc93747a945a9d9bff10b141a5287f0c8429a08828a22192 \ 520 | --hash=sha256:06ba73b149bf838d5cd25dc30c2dd2671ae5b2757cf98e5c41a35fe449f131b3 521 | tqdm==4.41.1 \ 522 | --hash=sha256:efab950cf7cc1e4d8ee50b2bb9c8e4a89f8307b49e0b2c9cfef3ec4ca26655eb \ 523 | --hash=sha256:4789ccbb6fc122b5a6a85d512e4e41fc5acad77216533a6f2b8ce51e0f265c23 524 | traitlets==4.3.3 \ 525 | --hash=sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44 \ 526 | --hash=sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7 527 | twine==3.1.1 \ 528 | --hash=sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124 \ 529 | --hash=sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160 530 | typed-ast==1.4.0 \ 531 | --hash=sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e \ 532 | --hash=sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b \ 533 | --hash=sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4 \ 534 | --hash=sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12 \ 535 | --hash=sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631 \ 536 | --hash=sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233 \ 537 | --hash=sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1 \ 538 | --hash=sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a \ 539 | --hash=sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c \ 540 | --hash=sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a \ 541 | --hash=sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e \ 542 | --hash=sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d \ 543 | --hash=sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36 \ 544 | --hash=sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0 \ 545 | --hash=sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66 \ 546 | --hash=sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2 \ 547 | --hash=sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47 \ 548 | --hash=sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161 \ 549 | --hash=sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e \ 550 | --hash=sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34 551 | typing-extensions==3.7.4.1 \ 552 | --hash=sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d \ 553 | --hash=sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575 \ 554 | --hash=sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2 555 | urllib3==1.25.7 \ 556 | --hash=sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293 \ 557 | --hash=sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745 558 | vcrpy==4.0.2 \ 559 | --hash=sha256:c4ddf1b92c8a431901c56a1738a2c797d965165a96348a26f4b2bbc5fa6d36d9 \ 560 | --hash=sha256:9740c5b1b63626ec55cefb415259a2c77ce00751e97b0f7f214037baaf13c7bf 561 | virtualenv==16.7.9 \ 562 | --hash=sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb \ 563 | --hash=sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3 564 | wcwidth==0.1.8 \ 565 | --hash=sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603 \ 566 | --hash=sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8 567 | webencodings==0.5.1 \ 568 | --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ 569 | --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 570 | wrapt==1.11.2 \ 571 | --hash=sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1 572 | yarl==1.4.2; python_version >= "3.6" \ 573 | --hash=sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b \ 574 | --hash=sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1 \ 575 | --hash=sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080 \ 576 | --hash=sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a \ 577 | --hash=sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f \ 578 | --hash=sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea \ 579 | --hash=sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb \ 580 | --hash=sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70 \ 581 | --hash=sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d \ 582 | --hash=sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce \ 583 | --hash=sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2 \ 584 | --hash=sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce \ 585 | --hash=sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b \ 586 | --hash=sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae \ 587 | --hash=sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462 \ 588 | --hash=sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6 \ 589 | --hash=sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b 590 | zipp==0.6.0; python_version < "3.8" \ 591 | --hash=sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335 \ 592 | --hash=sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e 593 | -------------------------------------------------------------------------------- /requirements.tox.txt: -------------------------------------------------------------------------------- 1 | requests<=2.22.0 2 | jsonschema<=3.2.0 3 | codecov<=2.0 4 | coverage<=5.0 5 | pydocstyle<=5.0 6 | pytest-cov<=2.8 7 | pytest-vcr<=1.0 8 | pytest<=5.3 9 | python-dotenv<=0.10.3 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==19.3.0 \ 2 | --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ 3 | --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 4 | certifi==2019.11.28 \ 5 | --hash=sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3 \ 6 | --hash=sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f 7 | chardet==3.0.4 \ 8 | --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \ 9 | --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae 10 | idna==2.8 \ 11 | --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c \ 12 | --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 13 | importlib-metadata==1.3.0; python_version < "3.8" \ 14 | --hash=sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f \ 15 | --hash=sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45 16 | jsonschema==3.2.0 \ 17 | --hash=sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163 \ 18 | --hash=sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a 19 | more-itertools==8.0.2 \ 20 | --hash=sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d \ 21 | --hash=sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564 22 | pyrsistent==0.15.6 \ 23 | --hash=sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b 24 | requests==2.22.0 \ 25 | --hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31 \ 26 | --hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 27 | six==1.13.0 \ 28 | --hash=sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd \ 29 | --hash=sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66 30 | urllib3==1.25.7 \ 31 | --hash=sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293 \ 32 | --hash=sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745 33 | zipp==0.6.0; python_version < "3.8" \ 34 | --hash=sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335 \ 35 | --hash=sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e 36 | -------------------------------------------------------------------------------- /resources/carbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/culqi/culqi-python/846c69d410528244282bc2b46d2c7f50e5219a90/resources/carbon.png -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/culqi/culqi-python/846c69d410528244282bc2b46d2c7f50e5219a90/resources/logo.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [flake8] 5 | exclude = 6 | .*/, 7 | __pycache__/, 8 | ignore = H101,H238,H301,H306,W503,F401,E231 9 | max-line-length = 88 10 | 11 | [pep8] 12 | exclude = 13 | .*/, 14 | __pycache__/, 15 | node_modules/, 16 | */migrations/ 17 | ignore = H101,H238,H301,H306,W503,F401 18 | max-line-length = 88 19 | 20 | [pydocstyle] 21 | ignore = D100, D101, D102, D103, D104, D105, D106, D107, D203, D213, D407, D202 22 | inherit = false 23 | match-dir = culqi_python 24 | 25 | [isort] 26 | skip = 27 | .direnv 28 | .tox 29 | .venv 30 | migrations 31 | node_modules 32 | not_skip = __init__.py 33 | 34 | # Vertical Hanging Indent 35 | multi_line_output = 3 36 | include_trailing_comma: True 37 | 38 | line_length = 88 39 | known_first_party = culqi 40 | known_third_party = 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | from setuptools import find_packages, setup 6 | 7 | package_name = "culqi-python-oficial" 8 | package_path = os.path.abspath(os.path.dirname(__file__)) 9 | repositorty_url = "https://github.com/culqi/culqi" 10 | long_description_file_path = os.path.join(package_path, "README.md") 11 | long_description = "" 12 | 13 | try: 14 | with open(long_description_file_path, encoding="utf-8") as f: 15 | long_description = f.read() 16 | except IOError: 17 | pass 18 | 19 | 20 | setup( 21 | name=package_name, 22 | packages=find_packages(exclude=[".*", "docs", "scripts", "tests*", "legacy",]), 23 | include_package_data=True, 24 | version=__import__("culqi").__version__, 25 | description="""Biblioteca de Culqi para Python""", 26 | long_description=long_description, 27 | long_description_content_type="text/markdown", 28 | author="Team Culqi", 29 | zip_safe=False, 30 | keywords=["Api Client", "Payment Integration", "Culqi", "Python 3", "Python 2",], 31 | classifiers=[ 32 | "Development Status :: 5 - Production/Stable", 33 | "Intended Audience :: Developers", 34 | "License :: OSI Approved :: MIT License", 35 | "Natural Language :: English", 36 | "Programming Language :: Python :: 2.7", 37 | "Programming Language :: Python :: 3.5", 38 | "Programming Language :: Python :: 3.6", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: 3.8", 41 | "Topic :: Software Development :: Libraries :: Python Modules", 42 | ], 43 | url=repositorty_url, 44 | download_url="%(url)s/-/archive/%(version)s/%(package)s-%(version)s.tar.gz" 45 | % { 46 | "url": repositorty_url, 47 | "version": __import__("culqi").__version__, 48 | "package": package_name, 49 | }, 50 | requires=["requests", "jsonschema"], 51 | install_requires=["requests", "jsonschema"], 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tests.test_card import CardTest 4 | from tests.test_charge import ChargeTest 5 | from tests.test_client import ClientTest 6 | from tests.test_customer import CustomerTest 7 | from tests.test_event import EventTest 8 | from tests.test_iin import IinTest 9 | from tests.test_order import OrderTest 10 | from tests.test_plan import PlanTest 11 | from tests.test_refund import RefundTest 12 | from tests.test_subscription import SubscriptionTest 13 | from tests.test_token import TokenTest 14 | from tests.test_transfer import TransferTest 15 | from tests.test_version import VersionTest 16 | 17 | if __name__ == "__main__": 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /tests/data.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class Data: 4 | TOKEN = { 5 | "cvv": "123", 6 | "card_number": "4111111111111111", 7 | "expiration_year": "2025", 8 | "expiration_month": "09", 9 | "email": "richard@piedpiper.com", 10 | } 11 | 12 | YAPE = { 13 | "amount": "36200", 14 | "fingerprint": "86d3c875769bf62b0471b47853bfda77", 15 | "number_phone": "900000001", 16 | "otp": "425251", 17 | } 18 | 19 | CHARGE = { 20 | "amount": 1000, 21 | "capture": False, 22 | "currency_code": "PEN", 23 | "description": "Venta de prueba", 24 | "email": "richard@piedpiper.com", 25 | "installments": 0, 26 | "source_id": None, 27 | } 28 | 29 | REFUND = { 30 | "amount": 100, 31 | "reason": "solicitud_comprador", 32 | "charge_id": None, 33 | } 34 | 35 | CUSTOMER = { 36 | "address": "Avenida Lima 123213", 37 | "address_city": "LIMA", 38 | "country_code": "PE", 39 | "email": "usuario@culqi.com", 40 | "first_name": "Richard", 41 | "last_name": "Piedpiper", 42 | "phone_number": "998989789", 43 | } 44 | 45 | PLAN = { 46 | "short_name": "cppe4", 47 | "description": "Cypress PCI ERRROR NO USAR", 48 | "amount": 300, 49 | "currency": "PEN", 50 | "interval_unit_time": 1, 51 | "interval_count": 1, 52 | "initial_cycles": { 53 | "count": 1, 54 | "has_initial_charge": False, 55 | "amount": 0, 56 | "interval_unit_time": 1 57 | }, 58 | "name": None, 59 | "metadata":{} 60 | } 61 | 62 | ORDER = { 63 | "amount": 1000, 64 | "currency_code": "PEN", 65 | "description": "Venta de prueba", 66 | "order_number": '12346shsbs_skd', 67 | "client_details": { 68 | "first_name": "Richard", 69 | "last_name": "Piedpiper", 70 | "email": "richard@piedpiper.com", 71 | "phone_number": "+51998989789", 72 | }, 73 | "expiration_date": int(time.time()) + 3600, 74 | "confirm": False, 75 | } 76 | -------------------------------------------------------------------------------- /tests/test_card.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from dotenv import load_dotenv 8 | 9 | from culqi import __version__ 10 | from culqi.client import Culqi 11 | from culqi.resources import Card 12 | from culqi.utils.urls import URL 13 | 14 | from tests.data import Data 15 | 16 | 17 | class CardTest(unittest.TestCase): 18 | def __init__(self, *args, **kwargs): 19 | super(CardTest, self).__init__(*args, **kwargs) 20 | load_dotenv() 21 | self.version = __version__ 22 | 23 | self.public_key = "pk_test_90667d0a57d45c48" 24 | self.private_key = "sk_test_1573b0e8079863ff" 25 | 26 | self.culqi = Culqi(self.public_key, self.private_key) 27 | self.card = Card(client=self.culqi) 28 | 29 | self.metadata = {"order_id": "0001"} 30 | 31 | @property 32 | def card_data(self): 33 | # pylint: disable=no-member 34 | email = "richard{0}@piedpiper.com".format(uuid4().hex[:4]) 35 | 36 | token_data = deepcopy(Data.TOKEN) 37 | token_data["email"] = email 38 | token = self.culqi.token.create(data=token_data) 39 | customer_data = deepcopy(Data.CUSTOMER) 40 | customer_data["email"] = email 41 | customer = self.culqi.customer.create(data=customer_data) 42 | 43 | return { 44 | "token_id": token["data"]["id"], 45 | "customer_id": customer["data"]["id"], 46 | } 47 | 48 | def test_url(self): 49 | # pylint: disable=protected-access 50 | id_ = "sample_id" 51 | 52 | assert self.card._get_url() == f"{URL.BASE}/v2/cards" 53 | assert self.card._get_url(id_) == f"{URL.BASE}/v2/cards/{id_}" 54 | 55 | @pytest.mark.vcr() 56 | def test_card_create(self): 57 | card = self.card.create(data=self.card_data) 58 | assert card["data"]["object"] == "card" 59 | 60 | @pytest.mark.vcr() 61 | def test_card_retrieve(self): 62 | created_card = self.card.create(data=self.card_data) 63 | retrieved_card = self.card.read(created_card["data"]["id"]) 64 | assert created_card["data"]["id"] == retrieved_card["data"]["id"] 65 | 66 | @pytest.mark.vcr() 67 | def test_card_list(self): 68 | retrieved_card_list = self.card.list() 69 | assert "items" in retrieved_card_list["data"] 70 | 71 | @pytest.mark.vcr() 72 | def test_card_update(self): 73 | created_card = self.card.create(data=self.card_data) 74 | 75 | metadatada = {"metadata": self.metadata} 76 | updated_card = self.card.update(id_=created_card["data"]["id"], data=metadatada) 77 | 78 | assert created_card["data"]["id"] == created_card["data"]["id"] 79 | assert updated_card["data"]["metadata"] == self.metadata 80 | 81 | @pytest.mark.vcr() 82 | def test_card_delete(self): 83 | created_card = self.card.create(data=self.card_data) 84 | deleted_card = self.card.delete(id_=created_card["data"]["id"]) 85 | 86 | assert deleted_card["data"]["deleted"] 87 | assert deleted_card["data"]["id"] == created_card["data"]["id"] 88 | assert deleted_card["status"] == 200 89 | 90 | 91 | if __name__ == "__main__": 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /tests/test_charge.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | 5 | import pytest 6 | from dotenv import load_dotenv 7 | 8 | from culqi import __version__ 9 | from culqi.client import Culqi 10 | from culqi.resources import Charge 11 | from culqi.utils.urls import URL 12 | 13 | from tests.data import Data 14 | 15 | 16 | class ChargeTest(unittest.TestCase): 17 | def __init__(self, *args, **kwargs): 18 | super(ChargeTest, self).__init__(*args, **kwargs) 19 | load_dotenv() 20 | self.version = __version__ 21 | self.public_key = "pk_test_90667d0a57d45c48" 22 | self.private_key = "sk_test_1573b0e8079863ff" 23 | self.culqi = Culqi(self.public_key, self.private_key) 24 | self.charge = Charge(client=self.culqi) 25 | self.metadata = {"order_id": "0001"} 26 | 27 | #ecnrypt variables 28 | self.rsa_public_key = "-----BEGIN PUBLIC KEY-----\n" + \ 29 | "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDuCmwMoEzvBk++m4rZUlZL4pDD\n" + \ 30 | "W++NV1tSjAOJsRv5Ermg3/ygjINNhi1gfMbfSiWloc85tJBZhXzD7JpOd7JxOOg7\n" + \ 31 | "CicgbZKGF/sq2geoVw4+n4j4vUZx0+a1PgStwR+BeZn2I+eAn9xOrHJD6/baJqIO\n" + \ 32 | "/ifGJ1e5jHeQXIR4IwIDAQAB\n" + \ 33 | "-----END PUBLIC KEY-----" 34 | self.rsa_id = "30b83fd0-8709-4fe4-86c1-fef042c3c2c3" 35 | 36 | @property 37 | def charge_data(self): 38 | # pylint: disable=no-member 39 | token_data = deepcopy(Data.TOKEN) 40 | token = self.culqi.token.create(data=token_data) 41 | charge_data = deepcopy(Data.CHARGE) 42 | charge_data["source_id"] = token["data"]["id"] 43 | print(charge_data) 44 | 45 | return charge_data 46 | 47 | def test_url(self): 48 | # pylint: disable=protected-access 49 | id_ = "sample_id" 50 | 51 | assert self.charge._get_url() == f"{URL.BASE}/v2/charges" 52 | assert self.charge._get_url( 53 | id_ 54 | ) == f"{URL.BASE}/v2/charges/{id_}" 55 | assert self.charge._get_url( 56 | id_, "capture" 57 | ) == f"{URL.BASE}/v2/charges/{id_}/capture" 58 | 59 | @pytest.mark.vcr() 60 | def test_charge_create(self): 61 | charge = self.charge.create(data=self.charge_data) 62 | assert charge["data"]["object"] == "charge" 63 | 64 | @pytest.mark.vcr() 65 | def test_charge_create_encrypt(self): 66 | options = {} 67 | options["rsa_public_key"] = self.rsa_public_key 68 | options["rsa_id"] = self.rsa_id 69 | 70 | charge = self.charge.create(data=self.charge_data, **options) 71 | 72 | assert charge["data"]["object"] == "charge" 73 | 74 | @pytest.mark.vcr() 75 | def test_charge_capture(self): 76 | created_charge = self.charge.create(data=self.charge_data) 77 | captured_charge = self.charge.capture(id_=created_charge["data"]["id"]) 78 | 79 | print(created_charge) 80 | print(captured_charge) 81 | 82 | assert captured_charge["data"]["id"] == created_charge["data"]["id"] 83 | assert captured_charge["status"] == 201 84 | 85 | @pytest.mark.vcr() 86 | def test_charge_retrieve(self): 87 | created_charge = self.charge.create(data=self.charge_data) 88 | retrieved_charge = self.charge.read(created_charge["data"]["id"]) 89 | 90 | assert created_charge["data"]["id"] == retrieved_charge["data"]["id"] 91 | 92 | @pytest.mark.vcr() 93 | def test_charge_recurrent_header(self): 94 | created_charge = self.charge.create(data=self.charge_data, custom_headers={'X-Charge-Channel': 'recurrent'}) 95 | retrieved_charge = self.charge.read(created_charge["data"]["id"]) 96 | 97 | assert created_charge["data"]["id"] == retrieved_charge["data"]["id"] 98 | 99 | @pytest.mark.vcr() 100 | def test_charge_list(self): 101 | retrieved_charge_list = self.charge.list() 102 | assert "items" in retrieved_charge_list["data"] 103 | 104 | @pytest.mark.vcr() 105 | def test_charge_update(self): 106 | created_charge = self.charge.create(data=self.charge_data) 107 | 108 | metadatada = {"metadata": self.metadata} 109 | updated_charge = self.charge.update( 110 | id_=created_charge["data"]["id"], data=metadatada 111 | ) 112 | 113 | assert updated_charge["data"]["id"] == created_charge["data"]["id"] 114 | assert updated_charge["data"]["metadata"] == self.metadata 115 | 116 | 117 | if __name__ == "__main__": 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from dotenv import load_dotenv 5 | 6 | from culqi import __version__ 7 | from culqi.client import Culqi 8 | 9 | 10 | class ClientTest(unittest.TestCase): 11 | def __init__(self, *args, **kwargs): 12 | super(ClientTest, self).__init__(*args, **kwargs) 13 | load_dotenv() 14 | self.version = __version__ 15 | self.public_key = "pk_test_90667d0a57d45c48" 16 | self.private_key = "sk_test_1573b0e8079863ff" 17 | self.culqi = Culqi(self.public_key, self.private_key) 18 | 19 | def test_version(self): 20 | # pylint: disable=protected-access 21 | assert self.culqi._get_version() == self.version 22 | 23 | def test_keys(self): 24 | assert self.public_key == self.culqi.public_key 25 | assert self.private_key == self.culqi.private_key 26 | 27 | def test_session_headers(self): 28 | session_headers = self.culqi.session.headers 29 | headers = { 30 | "User-Agent": "Culqi-API-Python/{0}".format(self.version), 31 | "Authorization": "Bearer {0}".format(self.private_key), 32 | "Content-Type": "application/json", 33 | "Accept": "application/json", 34 | } 35 | 36 | assert headers["User-Agent"] == session_headers["User-Agent"] 37 | assert headers["Authorization"] == session_headers["Authorization"] 38 | assert headers["Content-Type"] == session_headers["Content-Type"] 39 | assert headers["Accept"] == session_headers["Accept"] 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/test_customer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from dotenv import load_dotenv 8 | 9 | 10 | from culqi import __version__ 11 | from culqi.client import Culqi 12 | from culqi.resources import Customer 13 | from culqi.utils.urls import URL 14 | 15 | from .data import Data 16 | 17 | 18 | class CustomerTest(unittest.TestCase): 19 | def __init__(self, *args, **kwargs): 20 | super(CustomerTest, self).__init__(*args, **kwargs) 21 | load_dotenv() 22 | self.version = __version__ 23 | self.public_key = "pk_test_90667d0a57d45c48" 24 | self.private_key = "sk_test_1573b0e8079863ff" 25 | self.culqi = Culqi(self.public_key, self.private_key) 26 | self.customer = Customer(client=self.culqi) 27 | 28 | self.customer_data = deepcopy(Data.CUSTOMER) 29 | self.customer_data["email"] = "richard{0}@piedpiper.com".format(uuid4().hex[:4]) 30 | self.metadata = {"order_id": "0001"} 31 | 32 | def test_url(self): 33 | # pylint: disable=protected-access 34 | id_ = "sample_id" 35 | 36 | assert self.customer._get_url() == f"{URL.BASE}/v2/customers" 37 | assert self.customer._get_url( 38 | id_ 39 | ) ==f"{URL.BASE}/v2/customers/{id_}" 40 | 41 | @pytest.mark.vcr() 42 | def test_customer_create(self): 43 | customer = self.customer.create(data=self.customer_data) 44 | assert customer["data"]["object"] == "customer" 45 | 46 | @pytest.mark.vcr() 47 | def test_customer_retrieve(self): 48 | created_customer = self.customer.create(data=self.customer_data) 49 | retrieved_customer = self.customer.read(created_customer["data"]["id"]) 50 | assert created_customer["data"]["id"] == retrieved_customer["data"]["id"] 51 | 52 | @pytest.mark.vcr() 53 | def test_customer_list(self): 54 | retrieved_customer_list = self.customer.list() 55 | assert "items" in retrieved_customer_list["data"] 56 | 57 | @pytest.mark.vcr() 58 | def test_customer_update(self): 59 | created_customer = self.customer.create(data=self.customer_data) 60 | 61 | metadatada = {"metadata": self.metadata} 62 | updated_customer = self.customer.update( 63 | id_=created_customer["data"]["id"], data=metadatada 64 | ) 65 | 66 | assert created_customer["data"]["id"] == created_customer["data"]["id"] 67 | assert updated_customer["data"]["metadata"] == self.metadata 68 | 69 | @pytest.mark.vcr() 70 | def test_customer_delete(self): 71 | created_customer = self.customer.create(data=self.customer_data) 72 | deleted_customer = self.customer.delete(id_=created_customer["data"]["id"]) 73 | 74 | assert deleted_customer["data"]["deleted"] 75 | assert deleted_customer["data"]["id"] == created_customer["data"]["id"] 76 | assert deleted_customer["status"] == 200 77 | 78 | 79 | if __name__ == "__main__": 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /tests/test_event.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import pytest 5 | from dotenv import load_dotenv 6 | 7 | from culqi import __version__ 8 | from culqi.client import Culqi 9 | from culqi.utils.urls import URL 10 | 11 | from culqi.resources import Event 12 | 13 | 14 | class EventTest(unittest.TestCase): 15 | def __init__(self, *args, **kwargs): 16 | super(EventTest, self).__init__(*args, **kwargs) 17 | load_dotenv() 18 | self.version = __version__ 19 | self.public_key = "pk_test_90667d0a57d45c48" 20 | self.private_key = "sk_test_1573b0e8079863ff" 21 | self.culqi = Culqi(self.public_key, self.private_key) 22 | self.event = Event(client=self.culqi) 23 | 24 | def test_url(self): 25 | # pylint: disable=protected-access 26 | id_ = "sample_id" 27 | 28 | assert self.event._get_url() == f"{URL.BASE}/v2/events" 29 | assert self.event._get_url(id_) == f"{URL.BASE}/v2/events/{id_}" 30 | 31 | # @pytest.mark.vcr() 32 | # def test_event_retrieve(self): 33 | # retrieved_event = self.event.read(created_event["data"]["id"]) 34 | # assert created_event["data"]["id"] == retrieved_event["data"]["id"] 35 | 36 | @pytest.mark.vcr() 37 | def test_event_list(self): 38 | retrieved_event_list = self.event.list() 39 | assert "items" in retrieved_event_list["data"] 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/test_iin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import pytest 5 | from dotenv import load_dotenv 6 | 7 | from culqi import __version__ 8 | from culqi.client import Culqi 9 | from culqi.utils.urls import URL 10 | 11 | from culqi.resources import Iin 12 | 13 | 14 | class IinTest(unittest.TestCase): 15 | def __init__(self, *args, **kwargs): 16 | super(IinTest, self).__init__(*args, **kwargs) 17 | load_dotenv() 18 | self.version = __version__ 19 | self.public_key = "pk_test_90667d0a57d45c48" 20 | self.private_key = "sk_test_1573b0e8079863ff" 21 | self.culqi = Culqi(self.public_key, self.private_key) 22 | self.iin = Iin(client=self.culqi) 23 | 24 | def test_url(self): 25 | # pylint: disable=protected-access 26 | id_ = "sample_id" 27 | 28 | assert self.iin._get_url() == f"{URL.BASE}/v2/iins" 29 | assert self.iin._get_url(id_) == f"{URL.BASE}/v2/iins/{id_}" 30 | 31 | # @pytest.mark.vcr() 32 | # def test_iin_retrieve(self): 33 | # retrieved_iin = self.iin.read(created_iin["data"]["id"]) 34 | # assert created_iin["data"]["id"] == retrieved_iin["data"]["id"] 35 | 36 | @pytest.mark.vcr() 37 | def test_iin_list(self): 38 | retrieved_iin_list = self.iin.list() 39 | assert "items" in retrieved_iin_list["data"] 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/test_order.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from dotenv import load_dotenv 8 | 9 | from culqi import __version__ 10 | from culqi.client import Culqi 11 | from culqi.resources import Order 12 | from culqi.utils.urls import URL 13 | 14 | from .data import Data 15 | 16 | 17 | class OrderTest(unittest.TestCase): 18 | def __init__(self, *args, **kwargs): 19 | super(OrderTest, self).__init__(*args, **kwargs) 20 | load_dotenv() 21 | self.version = __version__ 22 | self.public_key = "pk_test_90667d0a57d45c48" 23 | self.private_key = "sk_test_1573b0e8079863ff" 24 | self.culqi = Culqi(self.public_key, self.private_key) 25 | self.order = Order(client=self.culqi) 26 | 27 | self.metadata = {"order_id": "0001"} 28 | 29 | #ecnrypt variables 30 | self.rsa_public_key = "-----BEGIN PUBLIC KEY-----\n"+\ 31 | "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYp0451xITpczkBrl5Goxkh7m1\n"+\ 32 | "oynj8eDHypIn7HmbyoNJd8cS4OsT850hIDBwYmFuwmxF1YAJS8Cd2nes7fjCHh+7\n"+\ 33 | "oNqgNKxM2P2NLaeo4Uz6n9Lu4KKSxTiIT7BHiSryC0+Dic91XLH7ZTzrfryxigsc\n"+\ 34 | "+ZNndv0fQLOW2i6OhwIDAQAB\n"+\ 35 | "-----END PUBLIC KEY-----\n" 36 | self.rsa_id = "508fc232-0a9d-4fc0-a192-364a0b782b89" 37 | 38 | @property 39 | def order_data(self): 40 | order_data = deepcopy(Data.ORDER) 41 | order_data["order_number"] = "order-{0}".format(uuid4().hex[:4]) 42 | 43 | return order_data 44 | 45 | def test_url(self): 46 | # pylint: disable=protected-access 47 | id_ = "sample_id" 48 | 49 | assert self.order._get_url() == f"{URL.BASE}/v2/orders" 50 | assert self.order._get_url(id_) == f"{URL.BASE}/v2/orders/{id_}" 51 | assert self.order._get_url( 52 | id_, "confirm" 53 | ) ==f"{URL.BASE}/v2/orders/{id_}/confirm" 54 | 55 | @pytest.mark.vcr() 56 | def test_order_create(self): 57 | order = self.order.create(data=self.order_data) 58 | 59 | assert order["data"]["object"] == "order" 60 | 61 | @pytest.mark.vcr() 62 | def test_order_create_encrypt(self): 63 | options = {} 64 | options["rsa_public_key"] = self.rsa_public_key 65 | options["rsa_id"] = self.rsa_id 66 | 67 | order = self.order.create(data=self.order_data, **options) 68 | 69 | assert order["data"]["object"] == "order" 70 | 71 | @pytest.mark.vcr() 72 | def test_order_confirm(self): 73 | created_order = self.order.create(data=self.order_data) 74 | confirmed_order = self.order.confirm(created_order["data"]["id"]) 75 | 76 | assert confirmed_order["data"]["id"] == created_order["data"]["id"] 77 | assert confirmed_order["status"] == 201 78 | 79 | @pytest.mark.vcr() 80 | def test_order_retrieve(self): 81 | created_order = self.order.create(data=self.order_data) 82 | retrieved_order = self.order.read(created_order["data"]["id"]) 83 | 84 | assert created_order["data"]["id"] == retrieved_order["data"]["id"] 85 | 86 | @pytest.mark.vcr() 87 | def test_order_list(self): 88 | retrieved_order_list = self.order.list() 89 | assert "items" in retrieved_order_list["data"] 90 | 91 | @pytest.mark.vcr() 92 | def test_order_update(self): 93 | created_order = self.order.create(data=self.order_data) 94 | 95 | metadatada = {"metadata": self.metadata} 96 | updated_order = self.order.update( 97 | id_=created_order["data"]["id"], data=metadatada 98 | ) 99 | 100 | assert updated_order["data"]["id"] == created_order["data"]["id"] 101 | assert updated_order["data"]["metadata"] == self.metadata 102 | 103 | # Failing test: can't delete orders 104 | # @pytest.mark.vcr() 105 | # def test_order_delete(self): 106 | # created_order = self.order.create(data=self.order_data) 107 | # confirmed_order = self.order.confirm(id_=created_order['data']['id']) 108 | # deleted_order = self.order.delete(id_=confirmed_order["data"]["id"]) 109 | # assert deleted_order["data"]["deleted"] 110 | # assert deleted_order["data"]["id"] == created_order["data"]["id"] 111 | # assert deleted_order["status"] == 200 112 | 113 | 114 | if __name__ == "__main__": 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /tests/test_plan.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from dotenv import load_dotenv 8 | 9 | from culqi import __version__ 10 | from culqi.client import Culqi 11 | from culqi.resources import Plan 12 | from culqi.utils.urls import URL 13 | 14 | from .data import Data 15 | 16 | 17 | class PlanTest(unittest.TestCase): 18 | def __init__(self, *args, **kwargs): 19 | super(PlanTest, self).__init__(*args, **kwargs) 20 | load_dotenv() 21 | self.version = __version__ 22 | self.public_key = "pk_test_90667d0a57d45c48" 23 | self.private_key = "sk_test_1573b0e8079863ff" 24 | self.culqi = Culqi(self.public_key, self.private_key) 25 | self.plan = Plan(client=self.culqi) 26 | 27 | self.metadata = {"order_id": "0001"} 28 | 29 | @property 30 | def plan_data(self): 31 | plan_data = deepcopy(Data.PLAN) 32 | plan_data["name"] = "plan-{0}".format(uuid4().hex[:4]) 33 | 34 | return plan_data 35 | 36 | def test_url(self): 37 | # pylint: disable=protected-access 38 | id_ = "sample_id" 39 | 40 | assert self.plan._get_url() == f"{URL.BASE}/v2/recurrent/plans" 41 | assert self.plan._get_url(id_) == f"{URL.BASE}/v2/recurrent/plans/{id_}" 42 | 43 | #python3 -m pytest -k test_plan_create -p no:warnings 44 | @pytest.mark.vcr() 45 | def test_plan_create(self): 46 | plan = self.plan.create(data=self.plan_data) 47 | assert "id" in plan["data"] and isinstance(plan["data"]["id"], str) 48 | 49 | #python3 -m pytest -k test_plan_retrieve -p no:warnings 50 | @pytest.mark.vcr() 51 | def test_plan_retrieve(self): 52 | created_plan = self.plan.create(data=self.plan_data) 53 | retrieved_plan = self.plan.read(created_plan["data"]["id"]) 54 | assert created_plan["data"]["id"] == retrieved_plan["data"]["id"] 55 | 56 | #python3 -m pytest -k test_plan_list -p no:warnings 57 | @pytest.mark.vcr() 58 | def test_plan_list(self): 59 | data_filter = { 60 | #"before": "pln_live_**********", 61 | #"after": "pln_live_**********", 62 | "limit": 1, 63 | #"min_amount": 300, 64 | #"max_amount": 500000, 65 | #"status": 1, 66 | #"creation_date_from": "1712692203", 67 | #"creation_date_to": "1712692203", 68 | } 69 | retrieved_plan_list = self.plan.list(data=data_filter) 70 | assert "items" in retrieved_plan_list["data"] 71 | 72 | #python3 -m pytest -k test_plan_update -p no:warnings 73 | @pytest.mark.vcr() 74 | def test_plan_update(self): 75 | created_plan = self.plan.create(data=self.plan_data) 76 | 77 | data_update = { 78 | "metadata": self.metadata, 79 | "status": 1, 80 | "name": "plan-{0}".format(uuid4().hex[:4]), 81 | "short_name": "short_plan-{0}".format(uuid4().hex[:4]), 82 | "description": "description", 83 | } 84 | 85 | updated_plan = self.plan.update(id_=created_plan["data"]["id"], data=data_update) 86 | assert created_plan["data"]["id"] == created_plan["data"]["id"] 87 | assert updated_plan["data"]["metadata"] == self.metadata 88 | 89 | #python3 -m pytest -k test_plan_delete -p no:warnings 90 | @pytest.mark.vcr() 91 | def test_plan_delete(self): 92 | created_plan = self.plan.create(data=self.plan_data) 93 | deleted_plan = self.plan.delete(id_=created_plan["data"]["id"]) 94 | assert deleted_plan["data"]["deleted"] 95 | assert deleted_plan["data"]["id"] == created_plan["data"]["id"] 96 | assert deleted_plan["status"] == 200 97 | 98 | 99 | if __name__ == "__main__": 100 | unittest.main() 101 | -------------------------------------------------------------------------------- /tests/test_refund.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | 5 | import pytest 6 | from dotenv import load_dotenv 7 | 8 | from culqi import __version__ 9 | from culqi.client import Culqi 10 | from culqi.resources import Refund 11 | from culqi.utils.urls import URL 12 | 13 | from .data import Data 14 | 15 | 16 | class RefundTest(unittest.TestCase): 17 | def __init__(self, *args, **kwargs): 18 | super(RefundTest, self).__init__(*args, **kwargs) 19 | load_dotenv() 20 | self.version = __version__ 21 | self.public_key = "pk_test_90667d0a57d45c48" 22 | self.private_key = "sk_test_1573b0e8079863ff" 23 | self.culqi = Culqi(self.public_key, self.private_key) 24 | self.refund = Refund(client=self.culqi) 25 | 26 | self.metadata = {"order_id": "0001"} 27 | 28 | @property 29 | def refund_data(self): 30 | # pylint: disable=no-member 31 | token_data = deepcopy(Data.TOKEN) 32 | token = self.culqi.token.create(data=token_data) 33 | 34 | charge_data = deepcopy(Data.CHARGE) 35 | charge_data["source_id"] = token["data"]["id"] 36 | charge = self.culqi.charge.create(data=charge_data) 37 | 38 | refund_data = deepcopy(Data.REFUND) 39 | refund_data["charge_id"] = charge["data"]["id"] 40 | return refund_data 41 | 42 | def test_url(self): 43 | # pylint: disable=protected-access 44 | id_ = "sample_id" 45 | 46 | assert self.refund._get_url() == f"{URL.BASE}/v2/refunds" 47 | assert self.refund._get_url( 48 | id_ 49 | ) == f"{URL.BASE}/v2/refunds/{id_}" 50 | 51 | @pytest.mark.vcr() 52 | def test_refund_create(self): 53 | refund = self.refund.create(data=self.refund_data) 54 | assert refund["data"]["object"] == "refund" 55 | 56 | @pytest.mark.vcr() 57 | def test_refund_retrieve(self): 58 | created_refund = self.refund.create(data=self.refund_data) 59 | retrieved_refund = self.refund.read(created_refund["data"]["id"]) 60 | 61 | assert created_refund["data"]["id"] == retrieved_refund["data"]["id"] 62 | 63 | @pytest.mark.vcr() 64 | def test_refund_list(self): 65 | retrieved_refund_list = self.refund.list() 66 | assert "items" in retrieved_refund_list["data"] 67 | 68 | @pytest.mark.vcr() 69 | def test_refund_update(self): 70 | created_refund = self.refund.create(data=self.refund_data) 71 | 72 | metadatada = {"metadata": self.metadata} 73 | updated_refund = self.refund.update( 74 | id_=created_refund["data"]["id"], data=metadatada 75 | ) 76 | 77 | assert updated_refund["data"]["id"] == created_refund["data"]["id"] 78 | assert updated_refund["data"]["metadata"] == self.metadata 79 | 80 | 81 | if __name__ == "__main__": 82 | unittest.main() 83 | -------------------------------------------------------------------------------- /tests/test_subscription.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from dotenv import load_dotenv 8 | 9 | from culqi import __version__ 10 | from culqi.client import Culqi 11 | from culqi.resources import Subscription 12 | from culqi.utils.urls import URL 13 | 14 | from .data import Data 15 | 16 | 17 | class SubscriptionTest(unittest.TestCase): 18 | def __init__(self, *args, **kwargs): 19 | super(SubscriptionTest, self).__init__(*args, **kwargs) 20 | load_dotenv() 21 | self.version = __version__ 22 | self.public_key = "pk_test_90667d0a57d45c48" 23 | self.private_key = "sk_test_1573b0e8079863ff" 24 | self.culqi = Culqi(self.public_key, self.private_key) 25 | self.subscription = Subscription(client=self.culqi) 26 | 27 | self.metadata = {"order_id": "0001"} 28 | 29 | @property 30 | def subscription_data(self): 31 | # pylint: disable=no-member 32 | email = "richard{0}@piedpiper.com".format(uuid4().hex[:4]) 33 | 34 | token_data = deepcopy(Data.TOKEN) 35 | token_data["email"] = email 36 | token = self.culqi.token.create(data=token_data) 37 | 38 | customer_data = deepcopy(Data.CUSTOMER) 39 | customer_data["email"] = email 40 | customer = self.culqi.customer.create(data=customer_data) 41 | 42 | card_data = { 43 | "token_id": token["data"]["id"], 44 | "customer_id": customer["data"]["id"], 45 | } 46 | card = self.culqi.card.create(data=card_data) 47 | 48 | plan_data = deepcopy(Data.PLAN) 49 | plan_data["name"] = "plan-{0}".format(uuid4().hex[:4]) 50 | plan = self.culqi.plan.create(data=plan_data) 51 | 52 | return { 53 | "card_id": card["data"]["id"], 54 | "plan_id": plan["data"]["id"], 55 | "tyc": True 56 | } 57 | 58 | @property 59 | def subscription_data_update(self): 60 | # pylint: disable=no-member 61 | email = "richard{0}@piedpiper.com".format(uuid4().hex[:4]) 62 | 63 | token_data = deepcopy(Data.TOKEN) 64 | token_data["email"] = email 65 | token = self.culqi.token.create(data=token_data) 66 | 67 | customer_data = deepcopy(Data.CUSTOMER) 68 | customer_data["email"] = email 69 | customer = self.culqi.customer.create(data=customer_data) 70 | 71 | card_data = { 72 | "token_id": token["data"]["id"], 73 | "customer_id": customer["data"]["id"], 74 | } 75 | card = self.culqi.card.create(data=card_data) 76 | 77 | return { 78 | "card_id": card["data"]["id"], 79 | "metadata": self.metadata 80 | } 81 | 82 | def test_url(self): 83 | # pylint: disable=protected-access 84 | id_ = "sample_id" 85 | 86 | assert self.subscription._get_url() == f"{URL.BASE}/v2/recurrent/subscriptions" 87 | assert self.subscription._get_url( 88 | id_ 89 | ) ==f"{URL.BASE}/v2/recurrent/subscriptions/{id_}" 90 | 91 | #python3 -m pytest -k test_subscription_create -p no:warnings 92 | @pytest.mark.vcr() 93 | def test_subscription_create(self): 94 | subscription = self.subscription.create(data=self.subscription_data) 95 | assert "id" in subscription["data"] and isinstance(subscription["data"]["id"], str) 96 | 97 | #python3 -m pytest -k test_subscription_create -p no:warnings 98 | @pytest.mark.vcr() 99 | def test_subscription_retrieve(self): 100 | created_subscription = self.subscription.create(data=self.subscription_data) 101 | retrieved_subscription = self.subscription.read(created_subscription["data"]["id"]) 102 | assert (created_subscription["data"]["id"] == retrieved_subscription["data"]["id"]) 103 | 104 | #python3 -m pytest -k test_subscription_list -p no:warnings 105 | @pytest.mark.vcr() 106 | def test_subscription_list(self): 107 | data_filter = { 108 | #"before": "1712692203", 109 | #"after": "1712692203", 110 | "limit": 1 111 | #"creation_date_from": "2023-12-30T00:00:00.000Z", 112 | #"creation_date_to": "2023-12-20T00:00:00.000Z", 113 | } 114 | retrieved_subscription_list = self.subscription.list(data=data_filter) 115 | assert "items" in retrieved_subscription_list["data"] 116 | 117 | #python3 -m pytest -k test_subscription_update -p no:warnings 118 | @pytest.mark.vcr() 119 | def test_subscription_update(self): 120 | created_subscription = self.subscription.create(data=self.subscription_data) 121 | data_update = self.subscription_data_update 122 | 123 | updated_subscription = self.subscription.update( 124 | id_=created_subscription["data"]["id"], data=data_update 125 | ) 126 | assert updated_subscription["data"]["id"] == created_subscription["data"]["id"] 127 | 128 | #python3 -m pytest -k test_subscription_delete -p no:warnings 129 | @pytest.mark.vcr() 130 | def test_subscription_delete(self): 131 | created_subscription = self.subscription.create(data=self.subscription_data) 132 | deleted_subscription = self.subscription.delete(id_=created_subscription["data"]["id"]) 133 | 134 | assert deleted_subscription["data"]["deleted"] 135 | assert deleted_subscription["data"]["id"] == created_subscription["data"]["id"] 136 | assert deleted_subscription["status"] == 200 137 | 138 | 139 | if __name__ == "__main__": 140 | unittest.main() 141 | -------------------------------------------------------------------------------- /tests/test_token.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from copy import deepcopy 4 | 5 | import pytest 6 | from dotenv import load_dotenv 7 | 8 | from culqi import __version__ 9 | from culqi.client import Culqi 10 | from culqi.resources import Token 11 | 12 | from .data import Data 13 | 14 | 15 | class TokenTest(unittest.TestCase): 16 | def __init__(self, *args, **kwargs): 17 | super(TokenTest, self).__init__(*args, **kwargs) 18 | load_dotenv() 19 | self.version = __version__ 20 | self.public_key = "pk_test_90667d0a57d45c48" 21 | self.private_key = "sk_test_1573b0e8079863ff" 22 | self.culqi = Culqi(self.public_key, self.private_key) 23 | self.token = Token(client=self.culqi) 24 | 25 | self.token_data = deepcopy(Data.TOKEN) 26 | self.yape_data = deepcopy(Data.YAPE) 27 | self.metadata = {"order_id": "0001"} 28 | 29 | #ecnrypt variables 30 | self.rsa_public_key = "-----BEGIN PUBLIC KEY-----\n"+\ 31 | "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCgd5UbyW6+DysTvwvS6fKzXK7j\n"+\ 32 | "smjKWPSHcjw8sSTr3rARRYMmOKgDI2ZbfsfcBedHp0ZjsrL/2owXV4N+GYTjheuj\n"+\ 33 | "VQl2g0rdu9JpYbxe5zSXNptjIPYjwFOzIt5ODotcwgcurA6XlU63zZZuCcUQSgka\n"+\ 34 | "2B7gmga5bxUZA6dAHwIDAQAB\n"+\ 35 | "-----END PUBLIC KEY-----" 36 | self.rsa_id = "82d2e538-9b07-4f0b-b615-9d7f028c825b" 37 | 38 | @pytest.mark.vcr() 39 | def test_token_create(self): 40 | token = self.token.create(data=self.token_data) 41 | print(token) 42 | assert token["data"]["object"] == "token" 43 | 44 | @pytest.mark.vcr() 45 | def test_token_create_encrypt(self): 46 | options = {} 47 | options["rsa_public_key"] = self.rsa_public_key 48 | options["rsa_id"] = self.rsa_id 49 | token = self.token.create(data=self.token_data, **options) 50 | assert token["data"]["object"] == "token" 51 | 52 | @pytest.mark.vcr() 53 | def test_token_yape_create(self): 54 | token = self.token.createyape(data=self.yape_data) 55 | assert token["data"]["object"] == "token" 56 | 57 | #@pytest.mark.vcr() 58 | #def test_token_yape_create_encrypt(self): 59 | # options = {} 60 | # options["rsa_public_key"] = self.rsa_public_key 61 | # options["rsa_id"] = self.rsa_id 62 | # token = self.token.createyape(data=self.yape_data, **options) 63 | # assert token["data"]["object"] == "token" 64 | 65 | @pytest.mark.vcr() 66 | def test_token_retrieve(self): 67 | created_token = self.token.create(data=self.token_data) 68 | retrieved_token = self.token.read(created_token["data"]["id"]) 69 | assert created_token["data"]["id"] == retrieved_token["data"]["id"] 70 | 71 | @pytest.mark.vcr() 72 | def test_token_list(self): 73 | querystring = { 74 | #"creation_date": "1476132639", 75 | #"creation_date_from": "1476132639", 76 | #"card_type": "credito", 77 | "device_type": "movil", 78 | #"bin": "411111", 79 | #"country_code": "PE", 80 | "limit": "10", 81 | } 82 | retrieved_token_list = self.token.list(querystring) 83 | assert "items" in retrieved_token_list["data"] 84 | 85 | @pytest.mark.vcr() 86 | def test_token_update(self): 87 | metadatada = {"metadata": self.metadata} 88 | created_token = self.token.create(data=self.token_data) 89 | updated_token = self.token.update( 90 | id_=created_token["data"]["id"], data=metadatada 91 | ) 92 | 93 | assert created_token["data"]["id"] == created_token["data"]["id"] 94 | assert updated_token["data"]["metadata"] == self.metadata 95 | 96 | 97 | if __name__ == "__main__": 98 | unittest.main() 99 | -------------------------------------------------------------------------------- /tests/test_transfer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | # import pytest 5 | from dotenv import load_dotenv 6 | 7 | from culqi import __version__ 8 | from culqi.client import Culqi 9 | from culqi.resources import Transfer 10 | from culqi.utils.urls import URL 11 | 12 | 13 | class TransferTest(unittest.TestCase): 14 | def __init__(self, *args, **kwargs): 15 | super(TransferTest, self).__init__(*args, **kwargs) 16 | load_dotenv() 17 | self.version = __version__ 18 | self.public_key = "pk_test_90667d0a57d45c48" 19 | self.private_key = "sk_test_1573b0e8079863ff" 20 | self.culqi = Culqi(self.public_key, self.private_key) 21 | self.transfer = Transfer(client=self.culqi) 22 | 23 | def test_url(self): 24 | # pylint: disable=protected-access 25 | id_ = "sample_id" 26 | 27 | assert self.transfer._get_url() ==f"{URL.BASE}/v2/transfers" 28 | assert self.transfer._get_url( 29 | id_ 30 | ) ==f"{URL.BASE}/v2/transfers/{id_}" 31 | 32 | # @pytest.mark.vcr() 33 | # def test_transfer_retrieve(self): 34 | # retrieved_transfer = self.transfer.read(created_transfer["data"]["id"]) 35 | # assert created_transfer["data"]["id"] == retrieved_transfer["data"]["id"] 36 | 37 | # Failing test: Request time out 38 | # @pytest.mark.vcr() 39 | # def test_transfer_list(self): 40 | # retrieved_transfer_list = self.transfer.list() 41 | # assert "items" in retrieved_transfer_list["data"] 42 | 43 | 44 | if __name__ == "__main__": 45 | unittest.main() 46 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from culqi import __version__ 4 | 5 | 6 | class VersionTest(unittest.TestCase): 7 | @staticmethod 8 | def test_version(): 9 | assert __version__ == "1.0.0" 10 | 11 | 12 | if __name__ == "__main__": 13 | unittest.main() 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,35,36,37,38} 3 | skipsdist = True 4 | 5 | [testenv] 6 | passenv = 7 | CODECOV_* TOXENV CI TRAVIS TRAVIS_* 8 | DATABASE_URL PRIVATE_KEY QUERIES_RESULTS_PATH 9 | API_PUBLIC_KEY 10 | API_PRIVATE_KEY 11 | 12 | commands = 13 | pytest --cov --cov-report= 14 | codecov 15 | deps = 16 | -rrequirements.tox.txt 17 | 18 | [testenv:black] 19 | basepython = python3.8 20 | deps = 21 | black>=19.10b0 22 | commands = 23 | black --check . 24 | 25 | [testenv:flake8] 26 | basepython = python3.8 27 | deps = 28 | flake8>=3.7.0 29 | commands = 30 | flake8 culqi tests 31 | 32 | [testenv:pydocstyle] 33 | basepython = python3.8 34 | whitelist_externals = sh 35 | commands = 36 | sh -c 'find culqi -name "*.py" -type f | xargs pydocstyle -v' 37 | 38 | [travis] 39 | python = 40 | 2.7: py27 41 | 3.5: py35 42 | 3.6: py36 43 | 3.7: py37 44 | 3.8-dev: py38 45 | unignore_outcomes = True 46 | --------------------------------------------------------------------------------