├── tests ├── __init__.py ├── conftest.py ├── mocks.py ├── test_institutions.py ├── test_requisition.py ├── test_client.py ├── test_agreement.py └── test_accounts.py ├── example ├── __init__.py ├── .env.template ├── requirements.txt ├── resources │ └── _media │ │ ├── f_6_aspsp_accs.jpg │ │ ├── f_3_select_aspsp.png │ │ ├── f_4.1_ng_redirect.png │ │ ├── f_4_ng_agreement.jpg │ │ ├── f_5.3_aspsp_auth.jpg │ │ ├── f_5_aspsps_signin.png │ │ ├── f_5.1_aspsps_signin.jpg │ │ ├── f_5.2_aspsps_signin.jpg │ │ └── f_6.1_aspsp_confirmation.png ├── templates │ └── index.html ├── README.md ├── app.py └── .gitignore ├── nordigen ├── utils │ ├── __init__.py │ └── filter.py ├── __init__.py ├── types │ ├── http_enums.py │ ├── __init__.py │ └── types.py ├── api │ ├── __init__.py │ ├── institutions.py │ ├── requisitions.py │ ├── agreements.py │ └── accounts.py └── nordigen.py ├── .env.template ├── .github ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .flake8 ├── .pre-commit-config.yaml ├── pyproject.toml ├── LICENSE.txt ├── CHANGELOG.md ├── main.py ├── .gitignore ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nordigen/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nordigen/__init__.py: -------------------------------------------------------------------------------- 1 | from .nordigen import NordigenClient 2 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | SECRET_ID="SECRET_ID" 2 | SECRET_KEY="SECRET_KEY" 3 | -------------------------------------------------------------------------------- /example/.env.template: -------------------------------------------------------------------------------- 1 | SECRET_ID="YOUR_SECRET_ID" 2 | SECRET_KEY="YOUR_SECRET_KEY" 3 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.1 2 | Jinja2==3.0.1 3 | python-dotenv==0.19.1 4 | nordigen~=1.1 5 | -------------------------------------------------------------------------------- /example/resources/_media/f_6_aspsp_accs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_6_aspsp_accs.jpg -------------------------------------------------------------------------------- /example/resources/_media/f_3_select_aspsp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_3_select_aspsp.png -------------------------------------------------------------------------------- /example/resources/_media/f_4.1_ng_redirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_4.1_ng_redirect.png -------------------------------------------------------------------------------- /example/resources/_media/f_4_ng_agreement.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_4_ng_agreement.jpg -------------------------------------------------------------------------------- /example/resources/_media/f_5.3_aspsp_auth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_5.3_aspsp_auth.jpg -------------------------------------------------------------------------------- /example/resources/_media/f_5_aspsps_signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_5_aspsps_signin.png -------------------------------------------------------------------------------- /example/resources/_media/f_5.1_aspsps_signin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_5.1_aspsps_signin.jpg -------------------------------------------------------------------------------- /example/resources/_media/f_5.2_aspsps_signin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_5.2_aspsps_signin.jpg -------------------------------------------------------------------------------- /example/resources/_media/f_6.1_aspsp_confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nordigen/nordigen-python/HEAD/example/resources/_media/f_6.1_aspsp_confirmation.png -------------------------------------------------------------------------------- /nordigen/types/http_enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class HTTPMethod(Enum): 5 | GET = "GET" 6 | POST = "POST" 7 | PUT = "PUT" 8 | DELETE = "DELETE" 9 | -------------------------------------------------------------------------------- /nordigen/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .accounts import AccountApi 2 | from .agreements import AgreementsApi 3 | from .institutions import InstitutionsApi 4 | from .requisitions import RequisitionsApi 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Issue 2 | Issue goes here 3 | 4 | ## Proposed Changes 5 | * Change 1 6 | * Change 2 7 | 8 | ## Additional Info 9 | Any additional information 10 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | docstring-convention = all 3 | max-line-length = 79 4 | 5 | ignore = 6 | # pydocstyle 7 | D100 8 | D104 9 | D107 10 | D203 11 | D212 12 | D213 13 | D413 14 | D407 15 | D406 16 | W503 17 | D106 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 21.10b0 4 | hooks: 5 | - id: black 6 | 7 | - repo: local 8 | hooks: 9 | - id: pytest-check 10 | name: pytest-check 11 | entry: pytest 12 | language: system 13 | pass_filenames: false 14 | always_run: true 15 | -------------------------------------------------------------------------------- /nordigen/types/__init__.py: -------------------------------------------------------------------------------- 1 | from nordigen.types.http_enums import HTTPMethod 2 | from nordigen.types.types import ( 3 | AccountBalances, 4 | AccountData, 5 | AccountDetails, 6 | AgreementsList, 7 | EnduserAgreement, 8 | Institutions, 9 | Requisition, 10 | RequisitionDto, 11 | RequisitionList, 12 | TokenType, 13 | ) 14 | -------------------------------------------------------------------------------- /nordigen/utils/filter.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Dict, Optional 3 | 4 | 5 | class DataFilter: 6 | 7 | def filter_payload(self, data: Optional[Dict]) -> Dict: 8 | """ 9 | Filter falsy values from dictionary 10 | 11 | Args: 12 | data (Optional[Dict]): data dict 13 | 14 | Returns: 15 | Dict: filtered dictionary 16 | """ 17 | if data is None: 18 | return {} 19 | 20 | return {k: v for (k,v) in data.items() if v} 21 | 22 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | 5 | from nordigen import NordigenClient 6 | 7 | from .mocks import mocked_token 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def client(): 12 | """ 13 | Nordigen client fixture. 14 | 15 | Yields: 16 | [NordigenClient]: NordigenClient instance 17 | """ 18 | nordigen = NordigenClient(secret_id="SECRET_ID", secret_key="SECRET_KEY") 19 | with patch("requests.post") as mock_request: 20 | mock_request.return_value.json.return_value = mocked_token 21 | response = nordigen.generate_token() 22 | nordigen.token = response["access"] 23 | yield nordigen 24 | -------------------------------------------------------------------------------- /tests/mocks.py: -------------------------------------------------------------------------------- 1 | mocked_token = { 2 | "access": "access_token", 3 | "access_expires": 86400, 4 | "refresh": "refresh_token", 5 | "refresh_expires": 2592000, 6 | } 7 | 8 | 9 | def generate_mock(id): 10 | return { 11 | "count": 2, 12 | "results": [ 13 | { 14 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 15 | "created": "2022-02-21T15:41:40.350Z", 16 | "redirect": "https://gocardless.com", 17 | }, 18 | { 19 | "id": id, 20 | "created": "2022-02-21T15:41:40.350Z", 21 | "redirect": "https://gocardless.com", 22 | }, 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 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. Specify API endpoint 16 | 2. Specify response & error message 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | 27 | If you're having general trouble with your Nordigen integration or bank related issue, please reach out to our support via email [bank-account-data-support@gocardless.com](bank-account-data-support@gocardless.com) 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nordigen" 3 | version = "1.4.2" 4 | description = "Python client for GoCardless Bank Account Data API" 5 | authors = ["Nordigen Solutions "] 6 | license = "MIT" 7 | homepage = "https://github.com/nordigen/nordigen-python" 8 | repository = "https://github.com/nordigen/nordigen-python" 9 | readme = "README.md" 10 | keywords = ["GoCardless", "Nordigen", "Nordigen API", "OpenBanking"] 11 | include = ["CHANGELOG.md", "README.md", "LICENSE", "./nordigen/**/*"] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.8" 15 | requests = "^2.26.0" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest = "^6.2.5" 19 | black = "^21.10b0" 20 | pre-commit = "^2.15.0" 21 | isort = "^5.10.0" 22 | python-dotenv = "^0.19.2" 23 | 24 | [tool.black] 25 | line-length = 79 26 | include = '\.pyi?$' 27 | exclude = ''' 28 | /( 29 | | \.git 30 | | \.tox 31 | | \.venv 32 | | _build 33 | | buck-out 34 | | build 35 | | main.py 36 | | tests/ 37 | )/ 38 | ''' 39 | 40 | [build-system] 41 | requires = ["poetry-core>=1.0.0"] 42 | build-backend = "poetry.core.masonry.api" 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 SIA "Nordigen Solutions" 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.2] - 2025-04-07 4 | 5 | - Add maintenance notice to README 6 | 7 | ## [1.4.1] - 2024-10-22 8 | 9 | - Added account_selection parameter for POST /api/v2/requisitions 10 | 11 | ## [1.4.0] - 2024-02-09 12 | 13 | - Update base URL form ob.gocardless.com to bankaccountdata.gocardless.com 14 | 15 | ## [1.3.2] - 2023-06-21 16 | 17 | - Update URLs to represent GoCardless 18 | 19 | ## [1.3.1] - 2023-03-13 20 | 21 | - Add the response in the response kwargs when instantiating the `HTTPError` 22 | 23 | ## [1.3.0] - 2022-08-15 24 | 25 | - Add Premium endpoints 26 | - Add default base url 27 | - Update types 28 | 29 | ## [1.2.0] - 2022-07-21 30 | 31 | - Add default timeout for requests, allow it to be changed 32 | - Make country argument optional for `get_institutions` method to allow getting all institutions at once 33 | 34 | ## [1.1.0] - 2022-05-03 35 | 36 | - Add date_filter for transaction endpoint 37 | - Set client token property on token exchange 38 | 39 | ## [1.0.1] - 2022-01-13 40 | 41 | - Update documentation 42 | - Fix token setter 43 | - Move `python-dotenv` to dev dependencies 44 | 45 | ## [1.0.0] - 2022-01-12 46 | 47 | - Initial release 48 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 |
16 |
17 | × 18 |

Select your bank:

19 |
20 |
21 |
22 | 23 | 24 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /nordigen/types/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Dict, List, Optional, TypedDict 4 | 5 | 6 | class TokenType(TypedDict): 7 | access: str 8 | refresh: str 9 | access_expires: int 10 | refresh_expires: int 11 | 12 | 13 | class Institutions(TypedDict): 14 | id: str 15 | name: str 16 | bic: str 17 | transaction_total_days: str 18 | countries: List[str] 19 | logo: str 20 | 21 | 22 | class EnduserAgreement(TypedDict): 23 | id: str 24 | created: str 25 | enduser_id: str 26 | institution_id: str 27 | accepted: Optional[str] 28 | access_scope: List[str] 29 | max_historical_days: int 30 | access_valid_for_days: int 31 | 32 | 33 | class AgreementsList(TypedDict): 34 | count: int 35 | next: Optional[str] 36 | previous: Optional[str] 37 | results: List[EnduserAgreement] 38 | 39 | 40 | class Requisition(TypedDict): 41 | id: str 42 | created: str 43 | redirect: str 44 | status: str 45 | agreements: List[str] 46 | accounts: List[str] 47 | reference: str 48 | enduser_id: str 49 | user_language: Optional[str] 50 | results: List[Dict[str, str]] 51 | 52 | 53 | class RequisitionList(AgreementsList): 54 | results: List[Requisition] 55 | 56 | 57 | @dataclass 58 | class RequisitionDto: 59 | link: str 60 | requisition_id: str 61 | 62 | 63 | class Balances(TypedDict): 64 | amount: str 65 | currency: str 66 | 67 | 68 | class AccountBalances(TypedDict): 69 | balances: Balances 70 | balance_type: str 71 | credit_limit_included: Optional[bool] 72 | last_change_date_time: Optional[datetime] 73 | reference_date: Optional[datetime] 74 | last_committed_transaction: Optional[str] 75 | 76 | 77 | class AccountData(TypedDict): 78 | id: str 79 | created: datetime 80 | lastAccessed: datetime 81 | iban: str 82 | bban: Optional[str] 83 | institutionId: str 84 | status: str 85 | 86 | 87 | class AccountInfo(TypedDict): 88 | resource_id: str 89 | iban: str 90 | currency: str 91 | ownerName: str 92 | 93 | 94 | class AccountDetails(TypedDict): 95 | account: AccountInfo 96 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | from nordigen import NordigenClient 3 | 4 | def main(): 5 | # Load token from .env file or pass secrets as a string 6 | client = NordigenClient( 7 | secret_id="SECRET_ID", 8 | secret_key="SECRET_KEY" 9 | ) 10 | # generate access_token 11 | token_data = client.generate_token() 12 | 13 | # Use existing token 14 | client.token = "YOUR_TOKEN" 15 | 16 | # Exchange refresh token for new access token 17 | new_token = client.exchange_token(token_data["refresh"]) 18 | 19 | # Get institution_id by country and institution name 20 | institution_id = client.institution.get_institution_id_by_name( 21 | country="LV", institution="Revolut" 22 | ) 23 | # Initialize bank session 24 | init = client.initialize_session( 25 | # institution id 26 | institution_id=institution_id, 27 | # redirect url after successful authentication 28 | redirect_uri="https://gocardless.com", 29 | # additional layer of unique ID defined by you 30 | reference_id=str(uuid4()), 31 | ) 32 | print(init.link) 33 | 34 | # Get account id after you have completed authorization with a bank 35 | accounts = client.requisition.get_requisition_by_id( 36 | requisition_id=init.requisition_id 37 | ) 38 | 39 | # Get account id from the list. 40 | try: 41 | account_id = accounts["accounts"][0] 42 | except IndexError: 43 | raise ValueError( 44 | "Account list is empty. Make sure you have completed authorization with a bank." 45 | ) 46 | 47 | # Create account instance and provide your account id from previous step 48 | account = client.account_api(id=account_id) 49 | 50 | # Get account data 51 | meta_data = account.get_metadata() 52 | balances = account.get_balances() 53 | details = account.get_details() 54 | transactions = account.get_transactions() 55 | # Filter transactions by specific date range 56 | transactions = account.get_transactions(date_from="2021-12-01", date_to="2022-01-21") 57 | 58 | # Premium 59 | premium_transactions = account.get_premium_transactions( 60 | country="LV", 61 | date_from="2021-12-01", 62 | date_to="2022-01-21" 63 | ) 64 | premium_details = account.get_premium_details() 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | /**/__pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | .pybuilder/ 70 | target/ 71 | 72 | # IPython 73 | profile_default/ 74 | ipython_config.py 75 | 76 | # pyenv 77 | # For a library or package, you might want to ignore these files since the code is 78 | # intended to run in multiple environments; otherwise, check them in: 79 | .python-version 80 | 81 | # pipenv 82 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 83 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 84 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 85 | # install all needed dependencies. 86 | #Pipfile.lock 87 | 88 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 89 | __pypackages__/ 90 | 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | .dmypy.json 110 | dmypy.json 111 | 112 | # Pyre type checker 113 | .pyre/ 114 | 115 | # pytype static type analyzer 116 | .pytype/ 117 | 118 | # Cython debug symbols 119 | cython_debug/ 120 | 121 | .vscode/ 122 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Nordigen Python example with Flask 2 | 3 | ## Set-up 4 | 5 | You'll need to get your `SECRET_ID` and `SECRET_KEY` from the [GoCardless' Bank Account Data Portal](https://bankaccountdata.gocardless.com/). 6 | In **app.py** file provide the token as a parameter for `NordigenClient`. 7 | 8 | ```python 9 | import os 10 | 11 | # import Nordigen client 12 | from nordigen import NordigenClient 13 | 14 | # Init Nordigen client pass secret_id and secret_key generated from Nordigen portal 15 | # Parameters can be loaded from .env or passed as a string 16 | # You have to modify secrets in app.py file 17 | client = NordigenClient( 18 | secret_id=os.getenv("SECRET_ID"), 19 | secret_key=os.getenv("SECRET_KEY") 20 | ) 21 | 22 | # Generate access & refresh token 23 | # Note: access_token is automatically injected to other requests after you successfully obtain it 24 | client.generate_token() 25 | ``` 26 | 27 | To initialize session with a bank, you have to specify `COUNTRY` (a two-letter country code) and your `REDIRECT_URI`. 28 | 29 | Modify following variables in `app.py` file 30 | ```python 31 | COUNTRY = 'LV' 32 | REDIRECT_URI = 'http://127.0.0.1:5000/result' 33 | ``` 34 | 35 | 36 | ## Installation 37 | 38 | Install required dependencies 39 | 40 | ```bash 41 | pip install -r requirements.txt 42 | ``` 43 | 44 | Start Flask project 45 | 46 | ```bash 47 | flask run 48 | ``` 49 | 50 | Below is an example of the authentication process with Revolut. 51 | 52 | ### 1. Go to http://localhost:5000/ and select bank 53 |

54 | 55 |

56 | 57 | ### 2. Provide consent 58 |

59 | 60 | 61 |

62 | 63 | ### 3. Sign into bank (Institution) 64 |

65 | 66 | 67 | 68 |

69 | 70 |

71 | 72 |

73 | 74 | ### 4. Select accounts 75 |

76 | 77 |

78 | 79 | ### 5. You will be redirected to specified `redirect_uri` in our case it is `http://localhost:5000/` where details, balances and transactions will be returned from your bank account. 80 | -------------------------------------------------------------------------------- /nordigen/api/institutions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Final, List, Optional 4 | 5 | from nordigen.types import Institutions 6 | from nordigen.types.http_enums import HTTPMethod 7 | 8 | if TYPE_CHECKING: 9 | from nordigen import NordigenClient 10 | 11 | 12 | class InstitutionsApi: 13 | """ 14 | Institution API class. Used to fetch information about bank. 15 | 16 | Attributes 17 | --------- 18 | client(NordigenClient): Injectable NordigenClient object to make an http requests 19 | 20 | Returns: None 21 | """ 22 | 23 | ENDPOINT: Final = "aspsps" 24 | 25 | def __init__(self, client: NordigenClient) -> None: 26 | self.__request = client.request 27 | self.ENDPOINT = "institutions" 28 | 29 | def get_institutions(self, country: Optional[str] = None) -> List[Institutions]: 30 | """ 31 | Get all available Institutions (banks) in a given country or for all countries if 32 | country isn't specified. 33 | 34 | Args: 35 | country (str, optional): Two-character country code 36 | 37 | Returns: 38 | List[Institutions]: List of institutions in a given country 39 | """ 40 | url = self.ENDPOINT 41 | if country: 42 | url = f"{self.ENDPOINT}/?country={country}" 43 | 44 | return self.__request( 45 | HTTPMethod.GET, url 46 | ) 47 | 48 | def get_institution_by_id(self, id: str) -> Institutions: 49 | """ 50 | Get details about specific institution by its id. 51 | 52 | Args: 53 | id (str): institution id (bank id) 54 | 55 | Returns: 56 | Institutions: Institutions json object 57 | """ 58 | return self.__request(HTTPMethod.GET, f"{self.ENDPOINT}/{id}/") 59 | 60 | def get_institution_id_by_name( 61 | self, country: str, institution: str 62 | ) -> str: 63 | """ 64 | Get institution id by institution name. 65 | 66 | Args: 67 | country (str): Two-character country code 68 | institution (str): Institution name (ex: Revolut) 69 | 70 | Raises: 71 | ValueError: If institution with given name is not found 72 | 73 | Returns: 74 | str: Institution id 75 | """ 76 | institutions = self.get_institutions(country) 77 | 78 | for bank in institutions: 79 | if institution.lower() in bank["name"].lower(): 80 | return bank["id"] 81 | 82 | raise ValueError(f"Institution: {institution} is not found") 83 | -------------------------------------------------------------------------------- /example/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from uuid import uuid4 3 | 4 | from dotenv import load_dotenv 5 | from flask import Flask, jsonify, redirect, render_template, session, url_for 6 | 7 | from nordigen import NordigenClient 8 | 9 | app = Flask(__name__) 10 | # set Flask secret key 11 | app.config["SECRET_KEY"] = os.urandom(24) 12 | 13 | COUNTRY = "LV" 14 | REDIRECT_URI = "http://127.0.0.1:5000/results" 15 | 16 | # Load secrets from .env file 17 | load_dotenv() 18 | 19 | # Init Nordigen client pass secret_id and secret_key generated from OB portal 20 | # In this example we will load secrets from .env file 21 | client = NordigenClient( 22 | secret_id=os.getenv("SECRET_ID"), 23 | secret_key=os.getenv("SECRET_KEY") 24 | ) 25 | 26 | # Generate access & refresh token 27 | client.generate_token() 28 | 29 | 30 | @app.route("/", methods=["GET"]) 31 | def home(): 32 | # Get list of institutions 33 | institution_list = client.institution.get_institutions(country=COUNTRY) 34 | return render_template("index.html", institutions=institution_list) 35 | 36 | 37 | @app.route("/agreements/", methods=["GET"]) 38 | def agreements(institution_id): 39 | 40 | if institution_id: 41 | 42 | init = client.initialize_session( 43 | institution_id=institution_id, 44 | redirect_uri=REDIRECT_URI, 45 | reference_id=str(uuid4()), 46 | ) 47 | 48 | redirect_url = init.link 49 | # save requisiton id to a session 50 | session["req_id"] = init.requisition_id 51 | return redirect(redirect_url) 52 | 53 | return redirect(url_for("home")) 54 | 55 | 56 | @app.route("/results", methods=["GET"]) 57 | def results(): 58 | 59 | if "req_id" in session: 60 | 61 | accounts = client.requisition.get_requisition_by_id( 62 | requisition_id=session["req_id"] 63 | )["accounts"] 64 | 65 | accounts_data = [] 66 | for id in accounts: 67 | account = client.account_api(id) 68 | metadata = account.get_metadata() 69 | transactions = account.get_transactions() 70 | details = account.get_details() 71 | balances = account.get_balances() 72 | 73 | accounts_data.append( 74 | { 75 | "metadata": metadata, 76 | "details": details, 77 | "balances": balances, 78 | "transactions": transactions, 79 | } 80 | ) 81 | 82 | return jsonify(accounts_data) 83 | 84 | raise Exception( 85 | "Requisition ID is not found. Please complete authorization with your bank" 86 | ) 87 | 88 | 89 | if __name__ == "__main__": 90 | app.run(debug=True) 91 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | /**/__pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .vscode/ -------------------------------------------------------------------------------- /tests/test_institutions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import MagicMock, Mock, patch 3 | 4 | from nordigen import NordigenClient 5 | from nordigen.api import InstitutionsApi 6 | 7 | 8 | class TestInstitutionsAPI(unittest.TestCase): 9 | """Test Institution API.""" 10 | 11 | def setUp(self) -> None: 12 | self.client = NordigenClient( 13 | secret_id="my_secret_id", secret_key="my_secret_key" 14 | ) 15 | self.mock_client = MagicMock(autospec=self.client) 16 | self.mock_institution = InstitutionsApi(client=self.mock_client) 17 | 18 | @patch.object(InstitutionsApi, "get_institutions") 19 | def test_get_institutions(self, mock_response: Mock): 20 | """ 21 | Test get institutions list. 22 | 23 | Args: 24 | mock_response (Mock): mocked institution object 25 | """ 26 | mock_response.return_value = [ 27 | { 28 | "id": "CITADELE_PARXLV22", 29 | "logo": "https://cdn-logos.gocardless.com/ais/CITADELE_PARXLV22.png", 30 | "name": "Citadele", 31 | "transaction_total_days": "730", 32 | }, 33 | { 34 | "id": "REVOLUT_REVOGB21", 35 | "name": "Revolut", 36 | "bic": "REVOGB21", 37 | "transaction_total_days": "730", 38 | "countries": ["GB"], 39 | "logo": "https://cdn-logos.gocardless.com/ais/REVOLUT_REVOGB21.png", 40 | }, 41 | ] 42 | response = self.mock_institution.get_institutions(country="LV") 43 | 44 | assert response[0]["id"] == "CITADELE_PARXLV22" 45 | assert response[1]["id"] == "REVOLUT_REVOGB21" 46 | 47 | @patch.object(InstitutionsApi, "get_institution_by_id") 48 | def test_get_institution_by_id(self, mock_response: Mock): 49 | """ 50 | Test get institution by id. 51 | 52 | Args: 53 | mock_response (Mock): [description] 54 | """ 55 | mock_response.return_value = { 56 | "id": "CITADELE_PARXLV22", 57 | "logo": "https://cdn-logos.gocardless.com/ais/CITADELE_PARXLV22.png", 58 | "name": "Citadele", 59 | "transaction_total_days": "730", 60 | } 61 | 62 | response = self.mock_institution.get_institution_by_id( 63 | id="CITADELE_PARXLV22" 64 | ) 65 | 66 | assert response["id"] == "CITADELE_PARXLV22" 67 | assert response["name"] == "Citadele" 68 | 69 | @patch.object(InstitutionsApi, "get_institution_id_by_name") 70 | def test_get_institution_id_by_name(self, mock_response: Mock): 71 | """ 72 | Test get institution id by institution name. 73 | 74 | Args: 75 | mock_response (Mock): mocked method 'get_institution_id_by_name' 76 | """ 77 | mock_response.return_value = "REVOLUT_REVOGB21" 78 | response = self.mock_institution.get_institution_id_by_name( 79 | institution="Revolut", country="LV" 80 | ) 81 | 82 | assert response == "REVOLUT_REVOGB21" 83 | -------------------------------------------------------------------------------- /tests/test_requisition.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | 6 | from nordigen.api import RequisitionsApi 7 | from nordigen.nordigen import NordigenClient 8 | 9 | from .mocks import generate_mock 10 | 11 | 12 | class TestRequisitionApi: 13 | """Test Requisition api.""" 14 | 15 | enduser_id = "1234567" 16 | redirect_uri = "https://ob.gocardless.com" 17 | institution_id = "REVOLUT_REVOGB21" 18 | requisition_id = "d49dffbb-01dc-498c-a674-8cb725aad14a" 19 | 20 | @pytest.fixture(scope="class") 21 | def requisition(self, client) -> RequisitionsApi: 22 | """Returns Agreement instance.""" 23 | return RequisitionsApi(client=client) 24 | 25 | def test_get_requisitions( 26 | self, requisition: RequisitionsApi, client: NordigenClient 27 | ): 28 | """ 29 | Test get requisition. 30 | 31 | Args: 32 | requisition (RequisitionsApi): Requisition instance 33 | client (NordigenClient): NordigenClient instance 34 | """ 35 | with patch("requests.get") as mock_request: 36 | mock_request.return_value.json.return_value = generate_mock( 37 | self.requisition_id 38 | ) 39 | response = requisition.get_requisitions() 40 | 41 | assert len(response["results"]) == 2 42 | assert ( 43 | response["results"][0]["id"] 44 | == "3fa85f64-5717-4562-b3fc-2c963f66afa6" 45 | ) 46 | assert ( 47 | mock.call( 48 | url=f"{client.base_url}/requisitions/", 49 | headers=client._headers, 50 | params={"limit": 100}, 51 | timeout = 10, 52 | ) 53 | in mock_request.call_args_list 54 | ) 55 | 56 | def test_delete_requisition(self, requisition: RequisitionsApi): 57 | """ 58 | Test delete requisition by id. 59 | 60 | Args: 61 | requisition (RequisitionsApi): Requisition instance 62 | """ 63 | with patch("requests.delete") as mock_request: 64 | mock_request.return_value.json.return_value = { 65 | "summary": "Requisition deleted" 66 | } 67 | response = requisition.delete_requisition( 68 | requisition_id=self.requisition_id 69 | ) 70 | assert response["summary"] == "Requisition deleted" 71 | 72 | def test_get_requisition_by_id(self, requisition: RequisitionsApi): 73 | """ 74 | Test get requisition by id for api v2. 75 | 76 | Args: 77 | requisition (RequisitionsApi): Requisition instance 78 | """ 79 | with patch("requests.get") as mock_request: 80 | mock_request.return_value.json.return_value = generate_mock( 81 | self.requisition_id 82 | ) 83 | response = requisition.get_requisition_by_id( 84 | requisition_id=self.requisition_id 85 | ) 86 | assert response["results"][1]["id"] == self.requisition_id 87 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from unittest import mock 4 | from unittest.mock import patch 5 | 6 | from nordigen import NordigenClient 7 | from nordigen.types.http_enums import HTTPMethod 8 | 9 | from tests.mocks import mocked_token 10 | 11 | 12 | class TestClient(unittest.TestCase): 13 | """Test Nordigen client.""" 14 | 15 | def setUp(self) -> None: 16 | """setUp.""" 17 | self.client = NordigenClient( 18 | secret_id="SECRET_ID", 19 | secret_key="SECRET_KEY", 20 | ) 21 | self.url = self.client.base_url 22 | 23 | @patch("requests.post") 24 | def test_generate_token(self, mock_request): 25 | """Test token generation.""" 26 | payload = { 27 | "secret_key": self.client.secret_key, 28 | "secret_id": self.client.secret_id, 29 | } 30 | 31 | mock_request.return_value.json.return_value = mocked_token 32 | response = self.client.generate_token() 33 | 34 | mock_request.assert_called_with( 35 | url=f"{self.url}/token/new/", 36 | headers=self.client._headers, 37 | data=json.dumps(payload), 38 | timeout = 10, 39 | ) 40 | 41 | assert response["access_expires"] == 86400 42 | assert response["access"] == mocked_token["access"] 43 | assert self.client.token == "access_token" 44 | 45 | @patch("requests.post") 46 | def test_exchange_token(self, mock_request): 47 | """Test token exchange.""" 48 | mock_request.return_value.json.return_value = { 49 | "access": "new_access_token", 50 | "access_expires": 86400, 51 | } 52 | 53 | response = self.client.exchange_token(refresh_token="refresh_token") 54 | assert self.client.token == "new_access_token" 55 | assert response["access"] == "new_access_token" 56 | 57 | @patch("requests.get") 58 | def test_get_request(self, mock_request): 59 | """ 60 | Test request with GET. 61 | 62 | Args: 63 | mock_request (Mock): Mock request 64 | """ 65 | mock_request.return_value.json.return_value = {"status": 200} 66 | response = self.client.request(HTTPMethod.GET, "sample") 67 | assert response["status"] == 200 68 | assert ( 69 | mock.call( 70 | url=f"{self.url}/sample", 71 | headers=self.client._headers, 72 | params={}, 73 | timeout = 10, 74 | ) 75 | in mock_request.call_args_list 76 | ) 77 | 78 | @patch("requests.post") 79 | def test_post_request(self, mock_request): 80 | """ 81 | Test request with POST. 82 | 83 | Args: 84 | mock_request (Mock): Mock request 85 | """ 86 | mock_request.return_value.json.return_value = {"status": 201} 87 | payload = {"data": "Post data"} 88 | response = self.client.request(HTTPMethod.POST, "sample", payload) 89 | assert response["status"] == 201 90 | assert ( 91 | mock.call( 92 | url=f"{self.url}/sample", 93 | headers=self.client._headers, 94 | data=json.dumps(payload), 95 | timeout = 10, 96 | ) 97 | in mock_request.call_args_list 98 | ) 99 | 100 | def test_client_raises_exception(self): 101 | """Test unsupported Http method.""" 102 | with self.assertRaises(Exception) as context: 103 | self.client.request(HTTPMethod.PATCH, "sample") 104 | 105 | assert str(context.exception) in 'Method "PATCH" is not supported' 106 | 107 | def test_token_setter(self): 108 | """Test token setter.""" 109 | self.client.token = "Token" 110 | assert self.client._headers["Authorization"] == "Bearer Token" 111 | -------------------------------------------------------------------------------- /nordigen/api/requisitions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Dict, Final, List 4 | 5 | from nordigen.types.http_enums import HTTPMethod 6 | from nordigen.types.types import Requisition 7 | 8 | if TYPE_CHECKING: 9 | from nordigen import NordigenClient 10 | 11 | 12 | class RequisitionsApi: 13 | """ 14 | Requsition API class related to requisitions. 15 | 16 | Attributes 17 | --------- 18 | client(NordigenClient): Injectable NordigenClient object to make an http requests 19 | 20 | Returns: None 21 | """ 22 | 23 | ENDPOINT: Final = "requisitions" 24 | 25 | def __init__(self, client: NordigenClient) -> None: 26 | self.__client = client 27 | 28 | def get_requisitions( 29 | self, limit: int = 100, offset: int = 0 30 | ) -> Requisition: 31 | """ 32 | Get list of requisitions. 33 | 34 | Args: 35 | limit (int, optional): number of results to return per page. Defaults to 100. 36 | offset (int, optional): the initial index from which to return the results. Defaults to 0. 37 | 38 | Returns: 39 | Requisition: json response with requisition details 40 | """ 41 | payload = {"limit": limit, "offset": offset} 42 | return self.__client.request( 43 | HTTPMethod.GET, f"{self.ENDPOINT}/", payload 44 | ) 45 | 46 | def create_requisition( 47 | self, 48 | redirect_uri: str, 49 | reference_id: str, 50 | institution_id: str = None, 51 | agreement: List[str] = None, 52 | user_language: str = None, 53 | account_selection: bool = None, 54 | ) -> Requisition: 55 | """ 56 | Create requisition for creating links and retrieving accounts. 57 | 58 | Args: 59 | redirect_uri (str): application redirect url 60 | reference_id (str): additional layer of unique ID defined by you 61 | enduser_id (str): a unique end-user ID of someone who's using your services, usually it's a UUID 62 | agreements (List[str] or str optional): agreement is provided as a string. 63 | user_language (str, optional): to enforce a language for all end user steps hosted 64 | by GoCardless passed as a two-letter country code. Defaults to None 65 | 66 | Returns: 67 | Requisition: [description] 68 | """ 69 | payload = { 70 | "redirect": redirect_uri, 71 | "reference": reference_id, 72 | "institution_id": institution_id, 73 | } 74 | 75 | if user_language: 76 | payload["user_language"] = user_language 77 | 78 | if agreement: 79 | payload["agreement"] = agreement 80 | 81 | if account_selection: 82 | payload["account_selection"] = account_selection 83 | 84 | return self.__client.request( 85 | HTTPMethod.POST, f"{self.ENDPOINT}/", payload 86 | ) 87 | 88 | def get_requisition_by_id(self, requisition_id: str) -> Requisition: 89 | """ 90 | Get list of requisitions. 91 | 92 | Args: 93 | requisition_id (str): A UUID string identifying this requisition. 94 | Returns: 95 | Requisition: account details 96 | """ 97 | return self.__client.request( 98 | HTTPMethod.GET, f"{self.ENDPOINT}/{requisition_id}/" 99 | ) 100 | 101 | def delete_requisition(self, requisition_id: str) -> Dict: 102 | """ 103 | Delete requisition by id. 104 | 105 | Args: 106 | requisition_id (str): A UUID string identifying this requisition. 107 | 108 | Returns: Dict that consist confirmation message that requisition has been deleted 109 | """ 110 | return self.__client.request( 111 | HTTPMethod.DELETE, f"{self.ENDPOINT}/{requisition_id}" 112 | ) 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nordigen Python 2 | 3 | ### ⚠️ Notice 4 | Please be advised that the Bank Account Data libraries are no longer actively updated or maintained. While these libraries may still function, GoCardless will not provide further updates, bug fixes, or support for them. 5 | # 6 | 7 | This is official Python client library for [GoCardless Bank Account Data](https://gocardless.com/bank-account-data/) API 8 | 9 | For a full list of endpoints and arguments, see the [docs](https://developer.gocardless.com/bank-account-data/quick-start-guide). 10 | 11 | Before starting to use API you will need to create a new secret and get your `SECRET_ID` and `SECRET_KEY` from the [GoCardless Bank Account Data Portal](https://bankaccountdata.gocardless.com/user-secrets/). 12 | 13 | ## Requirements 14 | 15 | * Python >= 3.8 16 | 17 | 18 | ## Installation 19 | 20 | Install library via pip package manager: 21 | 22 | ``` 23 | pip install nordigen 24 | ``` 25 | 26 | ## Example application 27 | 28 | Example code can be found in `main.py` file and Flask application can be found in the `example` directory 29 | 30 | ## Quickstart 31 | 32 | 33 | ```python 34 | from uuid import uuid4 35 | 36 | from nordigen import NordigenClient 37 | 38 | # initialize Nordigen client and pass SECRET_ID and SECRET_KEY 39 | client = NordigenClient( 40 | secret_id="SECRET_ID", 41 | secret_key="SECRET_KEY" 42 | ) 43 | 44 | # Create new access and refresh token 45 | # Parameters can be loaded from .env or passed as a string 46 | # Note: access_token is automatically injected to other requests after you successfully obtain it 47 | token_data = client.generate_token() 48 | 49 | # Use existing token 50 | client.token = "YOUR_TOKEN" 51 | 52 | # Exchange refresh token for new access token 53 | new_token = client.exchange_token(token_data["refresh"]) 54 | 55 | # Get institution id by bank name and country 56 | institution_id = client.institution.get_institution_id_by_name( 57 | country="LV", 58 | institution="Revolut" 59 | ) 60 | 61 | # Get all institution by providing country code in ISO 3166 format 62 | institutions = client.institution.get_institutions("LV") 63 | 64 | # Initialize bank session 65 | init = client.initialize_session( 66 | # institution id 67 | institution_id=institution_id, 68 | # redirect url after successful authentication 69 | redirect_uri="https://gocardless.com", 70 | # additional layer of unique ID defined by you 71 | reference_id=str(uuid4()) 72 | ) 73 | 74 | # Get requisition_id and link to initiate authorization process with a bank 75 | link = init.link # bank authorization link 76 | requisition_id = init.requisition_id 77 | ``` 78 | 79 | After successful authorization with a bank you can fetch your data (details, balances, transactions) 80 | 81 | --- 82 | 83 | ## Fetching account metadata, balances, details and transactions 84 | 85 | ```python 86 | 87 | # Get account id after you have completed authorization with a bank 88 | # requisition_id can be gathered from initialize_session response 89 | accounts = client.requisition.get_requisition_by_id( 90 | requisition_id=init.requisition_id 91 | ) 92 | 93 | # Get account id from the list. 94 | account_id = accounts["accounts"][0] 95 | 96 | # Create account instance and provide your account id from previous step 97 | account = client.account_api(id=account_id) 98 | 99 | # Fetch account metadata 100 | meta_data = account.get_metadata() 101 | # Fetch details 102 | details = account.get_details() 103 | # Fetch balances 104 | balances = account.get_balances() 105 | # Fetch transactions 106 | transactions = account.get_transactions() 107 | # Filter transactions by specific date range 108 | transactions = account.get_transactions(date_from="2021-12-01", date_to="2022-01-21") 109 | ``` 110 | 111 | ## Premium endpoints 112 | 113 | ```python 114 | # Get premium transactions. Country and date parameters are optional 115 | premium_transactions = account.get_premium_transactions( 116 | country="LV", 117 | date_from="2021-12-01", 118 | date_to="2022-01-21" 119 | ) 120 | # Get premium details 121 | premium_details = account.get_premium_details() 122 | ``` 123 | 124 | ## Support 125 | 126 | For any inquiries please contact support at [bank-account-data-support@gocardless.com](bank-account-data-support@gocardless.com) or create an issue in repository. 127 | -------------------------------------------------------------------------------- /nordigen/api/agreements.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Dict, Final, List, Union 4 | 5 | from nordigen.types.http_enums import HTTPMethod 6 | from nordigen.types.types import AgreementsList, EnduserAgreement 7 | 8 | if TYPE_CHECKING: 9 | from nordigen.client import NordigenClient 10 | 11 | 12 | class AgreementsApi: 13 | """ 14 | Agreements api class. 15 | 16 | Attributes 17 | ------- 18 | client (NordigenClient): Injectable NordigenClient object to make an http requests 19 | institution_id (str): institution_id (bank id) from institutions response. 20 | e.g {"id": "MONZO_MONZGB2L"} 21 | 22 | Returns: None 23 | """ 24 | 25 | __ENDPOINT: Final = "agreements/enduser" 26 | 27 | def __init__(self, client: NordigenClient) -> None: 28 | self.__client = client 29 | 30 | def create_agreement( 31 | self, 32 | institution_id: str, 33 | max_historical_days: int = 90, 34 | access_valid_for_days: int = 90, 35 | access_scope: Union[None, List[str]] = None, 36 | ) -> EnduserAgreement: 37 | """ 38 | Create end user agreement. 39 | 40 | Args: 41 | institution (str): Institution id. 42 | max_historical_days (int, optional): Length of the transaction history. Defaults to 90. 43 | access_valid_for_days (int, optional): access valid for days. 44 | access_scope (List[str], optional): 45 | access scope for account, by default provides access to balances, details, transactions. 46 | Defaults to [ "balances", "details", "transactions" ]. 47 | 48 | Returns: 49 | EnduserAgreement: Enduser agreement json object 50 | """ 51 | access_scope = access_scope or ["balances", "details", "transactions"] 52 | payload = { 53 | "max_historical_days": max_historical_days, 54 | "access_valid_for_days": access_valid_for_days, 55 | "access_scope": access_scope, 56 | "institution_id": institution_id, 57 | } 58 | 59 | return self.__client.request( 60 | HTTPMethod.POST, f"{self.__ENDPOINT}/", payload 61 | ) 62 | 63 | def get_agreements( 64 | self, limit: int = 100, offset: int = 0 65 | ) -> AgreementsList: 66 | """ 67 | Get list of agreements. 68 | 69 | Args: 70 | a unique end-user ID of someone who's using your services, it has to be unique within your solution. 71 | Usually, it's UUID 72 | limit (int, optional): number of results to return per page. Defaults to 100. 73 | offset (int, optional): the initial index from which to return the results. Defaults to 0. 74 | 75 | Returns: 76 | AgreementsList: json object with enduser agreements 77 | """ 78 | params = {"limit": limit, "offset": offset} 79 | return self.__client.request( 80 | HTTPMethod.GET, f"{self.__ENDPOINT}/", params 81 | ) 82 | 83 | def get_agreement_by_id(self, agreement_id: str) -> EnduserAgreement: 84 | """ 85 | Get agreement by agreement id. 86 | 87 | Args: 88 | agreement_id (str): id id from create_agreement response 89 | Returns: 90 | EnduserAgreement: JSON object with specific enduser agreements 91 | """ 92 | return self.__client.request( 93 | HTTPMethod.GET, f"{self.__ENDPOINT}/{agreement_id}" 94 | ) 95 | 96 | def delete_agreement(self, agreement_id: str) -> Dict: 97 | """ 98 | Delete End User Agreement by id. 99 | 100 | Args: 101 | agreement_id (str): A UUID string identifying this end user agreement. 102 | 103 | Returns: 104 | Dict: Dictionary with deleted agreement 105 | """ 106 | return self.__client.request( 107 | HTTPMethod.DELETE, f"{self.__ENDPOINT}/{agreement_id}" 108 | ) 109 | 110 | def accept_agreement( 111 | self, agreement_id: str, ip: str, user_agent: str 112 | ) -> Dict: 113 | """ 114 | Accept an end-user agreement via the API. 115 | 116 | Args: 117 | agreement_id (str): A UUID string identifying this end user agreement. 118 | ip (str): IP address of the client 119 | user_agent (str): [User Agent of the browser 120 | Returns: 121 | Dict: Dict with information on accepted agreement 122 | """ 123 | payload = {"user_agent": user_agent, "ip_address": ip} 124 | return self.__client.request( 125 | HTTPMethod.PUT, 126 | f"{self.__ENDPOINT}/{agreement_id}/accept/", 127 | payload, 128 | ) 129 | -------------------------------------------------------------------------------- /nordigen/api/accounts.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Final, Optional 4 | 5 | from nordigen.types.http_enums import HTTPMethod 6 | 7 | if TYPE_CHECKING: 8 | from nordigen import NordigenClient 9 | 10 | 11 | class AccountApi: 12 | """ 13 | Account API endpoints to fetch accounts, details, balances and transactions. 14 | 15 | Attributes 16 | --------- 17 | client (NordigenClient): 18 | Injectable NordigenClient object to make an http requests 19 | id (str): 20 | account id retrieved from request get_requisition_by_id() 21 | 22 | Returns: None 23 | """ 24 | 25 | __ENDPOINT: Final = "accounts" 26 | 27 | def __init__(self, client: NordigenClient, id: str) -> None: 28 | self.__request = client.request 29 | self.__id: str = id 30 | 31 | def __get(self, endpoint: str, parameters: dict = {}): 32 | """ 33 | Construct get request. 34 | 35 | Args: 36 | endpoint (str): endpoint 37 | 38 | Returns: 39 | [type]: [description] 40 | """ 41 | url = f"{self.__ENDPOINT}/{self.__id}/{endpoint}/" 42 | return self.__request(HTTPMethod.GET, f"{url}", parameters) 43 | 44 | 45 | def __getPremium(self, path, parameters: dict = {}): 46 | """ 47 | Construct get request for premium endpoints 48 | 49 | Args: 50 | path (_type_): _description_ 51 | parameters (dict, optional): _description_. Defaults to {}. 52 | 53 | Returns: 54 | _type_: _description_ 55 | """ 56 | url = f'{self.__ENDPOINT}/premium/{self.__id}/{path}' 57 | return self.__request(HTTPMethod.GET, f"{url}", parameters) 58 | 59 | def get_metadata(self) -> dict: 60 | """ 61 | Access account metadata. 62 | Information about the account record, such as the processing status and IBAN. 63 | Account status is recalculated based on the error count in the latest req. 64 | 65 | Returns: 66 | AccountData: account metadata 67 | """ 68 | return self.__request( 69 | HTTPMethod.GET, f"{self.__ENDPOINT}/{self.__id}/" 70 | ) 71 | 72 | def get_balances(self) -> dict: 73 | """ 74 | Access account balances. 75 | Balances will be returned in Berlin Group PSD2 format. 76 | 77 | Returns: 78 | dict: dictionary with balances 79 | """ 80 | return self.__get("balances") 81 | 82 | def get_details(self) -> dict: 83 | """ 84 | Access account details. 85 | Account details will be returned in Berlin Group PSD2 format. 86 | 87 | Returns: 88 | dict: dictionary with account details 89 | """ 90 | return self.__get("details") 91 | 92 | def get_transactions( 93 | self, 94 | date_from: Optional[str] = None, 95 | date_to: Optional[str] = None 96 | ) -> dict: 97 | """ 98 | Access account transactions. 99 | Transactions will be returned in Berlin Group PSD2 format. 100 | 101 | 102 | Returns: 103 | Dict: account transactions details 104 | """ 105 | date_range = { 106 | "date_from": date_from, 107 | "date_to": date_to 108 | } 109 | return self.__get("transactions", date_range) 110 | 111 | 112 | def get_premium_details(self, country: str = "") -> dict: 113 | """ 114 | Get premium details 115 | 116 | Args: 117 | country (str, optional): _description_. Defaults to "". 118 | 119 | Returns: 120 | dict: _description_ 121 | """ 122 | parameters = { 123 | "country": country 124 | } 125 | return self.__getPremium("details", parameters) 126 | 127 | def get_premium_balances(self) -> dict: 128 | """ 129 | Get premium balances 130 | 131 | Returns: 132 | dict: balances data 133 | """ 134 | return self.__getPremium("balances") 135 | 136 | def get_premium_transactions( 137 | self, 138 | country: Optional[str] = None, 139 | date_from: Optional[str] = None, 140 | date_to: Optional[str] = None 141 | ) -> dict: 142 | """ 143 | Get premium transactions 144 | 145 | Args: 146 | country (Optional[str], optional): country in iso format. Defaults to None. 147 | date_from (Optional[str], optional): date_from. Defaults to None. 148 | date_to (Optional[str], optional): date_to. Defaults to None. 149 | 150 | Returns: 151 | dict: dict with premium transactions 152 | """ 153 | parameters = { 154 | "date_from": date_from, 155 | "date_to": date_to, 156 | "country": country or "", 157 | } 158 | return self.__getPremium("transactions", parameters) 159 | -------------------------------------------------------------------------------- /tests/test_agreement.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest import mock 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from nordigen.api import AgreementsApi 8 | from nordigen.nordigen import NordigenClient 9 | 10 | from .mocks import generate_mock 11 | 12 | 13 | class TestAgreementApi: 14 | 15 | enduser_id = "1234567" 16 | institution_id = "REVOLUT_REVOGB21" 17 | agreement_id = "4bbf9b01-6c9e-431d-bfc6-11242429e991" 18 | 19 | @pytest.fixture(scope="class") 20 | def agreement(self, client) -> AgreementsApi: 21 | """Returns Agreement instance.""" 22 | return AgreementsApi(client=client) 23 | 24 | def test_get_agreement_by_id(self, agreement: AgreementsApi): 25 | """ 26 | Test get list of agreements by id. 27 | 28 | Args: 29 | agreement (AgreementsApi): AgreementsApi instance 30 | """ 31 | with patch("requests.get") as mocked_request: 32 | mocked_request.return_value.json.return_value = generate_mock( 33 | self.agreement_id 34 | ) 35 | response = agreement.get_agreement_by_id( 36 | agreement_id=self.agreement_id 37 | ) 38 | assert response["results"][1]["id"] == self.agreement_id 39 | 40 | def test_delete_agreement(self, agreement: AgreementsApi): 41 | """ 42 | Test delete agreement by id. 43 | 44 | Args: 45 | agreement (AgreementsApi): AgreementsApi instance 46 | """ 47 | with patch("requests.delete") as mock_request: 48 | mock_request.return_value.json.return_value = { 49 | "summary": "End User Agreement deleted" 50 | } 51 | response = agreement.delete_agreement( 52 | agreement_id=self.agreement_id 53 | ) 54 | assert response["summary"] == "End User Agreement deleted" 55 | 56 | def test_accept_agreement(self, agreement: AgreementsApi): 57 | """ 58 | Test accept end user agreement. 59 | 60 | Args: 61 | agreemen (AgreementsApi): AgreementsApi instance 62 | """ 63 | with patch("requests.put") as mock_request: 64 | mock_request.return_value.json.return_value = { 65 | "id": self.agreement_id, 66 | "accepted": True, 67 | } 68 | response = agreement.accept_agreement( 69 | user_agent="Chrome", 70 | ip="127.0.0.1", 71 | agreement_id=self.agreement_id, 72 | ) 73 | assert response["id"] == self.agreement_id 74 | 75 | def test_create_agreement( 76 | self, agreement: AgreementsApi, client: NordigenClient 77 | ): 78 | """ 79 | Test create agreement. 80 | 81 | Args: 82 | agreement (AgreementsApi): Agreement instance 83 | client: (NordigenClient): NordigenClient instance 84 | """ 85 | payload = { 86 | "max_historical_days": 90, 87 | "access_valid_for_days": 90, 88 | "access_scope": ["balances", "details", "transactions"], 89 | "institution_id": self.institution_id, 90 | } 91 | with patch("requests.post") as mock_request: 92 | mock_request.return_value.json.return_value = { 93 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 94 | "created": "2022-02-22T10:20:10.977Z", 95 | "max_historical_days": 90, 96 | "access_valid_for_days": 90, 97 | "institution_id": self.institution_id, 98 | } 99 | response = agreement.create_agreement(self.institution_id) 100 | assert response["institution_id"] == self.institution_id 101 | assert "enduser_id" not in response 102 | assert ( 103 | mock.call( 104 | url=f"{client.base_url}/agreements/enduser/", 105 | headers=client._headers, 106 | data=json.dumps(payload), 107 | timeout = 10, 108 | ) 109 | in mock_request.call_args_list 110 | ) 111 | 112 | def test_get_agreements( 113 | self, agreement: AgreementsApi, client: NordigenClient 114 | ): 115 | """ 116 | Test get agreements. 117 | 118 | Args: 119 | agreement (AgreementsApi): Agreement instance 120 | client: (NordigenClient): NordigenClient instance 121 | """ 122 | with patch("requests.get") as mock_request: 123 | mock_request.return_value.json.return_value = generate_mock( 124 | self.agreement_id 125 | ) 126 | response = agreement.get_agreements() 127 | assert response["results"][1]["id"] == self.agreement_id 128 | assert mock.call( 129 | url=f"{client.base_url}/agreements/enduser/", 130 | headers=client._headers, 131 | params={"limit": 100, "offset": 0}, 132 | ) 133 | -------------------------------------------------------------------------------- /tests/test_accounts.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from unittest.mock import patch 3 | from nordigen.nordigen import NordigenClient 4 | 5 | import pytest 6 | 7 | from nordigen.api import AccountApi 8 | 9 | 10 | class TestAccountApi: 11 | """Test Account api""" 12 | 13 | account_id = "1d2b827b-9ca2-4adb-b4c3-0deb76a0ac50" 14 | iban = "LT213250024324970797" 15 | mocked_data = { 16 | "details": { 17 | "account": { 18 | "resourceId": "534252452", 19 | "iban": iban, 20 | "currency": "EUR", 21 | } 22 | }, 23 | "balances": { 24 | "balanceAmount": { 25 | "amount": "657.49", 26 | "currency": "EUR", 27 | }, 28 | "balanceType": "EUR", 29 | }, 30 | "transactions": { 31 | "booked": [ 32 | { 33 | "trxAmount": { 34 | "currency": "EUR", 35 | "amount": "328.18", 36 | } 37 | } 38 | ], 39 | } 40 | } 41 | 42 | @pytest.fixture(scope="class") 43 | def account(self, client) -> AccountApi: 44 | """ 45 | Create Account api instance. 46 | 47 | Args: 48 | client (NordigenClient): return NordigenClient instance 49 | 50 | Returns: 51 | AccountApi: return AccountApi instance 52 | """ 53 | return AccountApi(client=client, id=self.account_id) 54 | 55 | def test_get_account_metadata(self, account: AccountApi): 56 | """ 57 | Test get account metadata. 58 | 59 | Args: 60 | account (AccountApi): AccountApi instance 61 | """ 62 | with patch("requests.get") as mock_request: 63 | mock_request.return_value.json.return_value = { 64 | "id": self.account_id, 65 | "created": "2022-02-22T10:37:34.556Z", 66 | "last_accessed": "2022-02-22T10:37:34.556Z", 67 | "iban": self.iban, 68 | } 69 | response = account.get_metadata() 70 | assert response["id"] == self.account_id 71 | 72 | def test_get_balances(self, account: AccountApi): 73 | """ 74 | Test get balances of an account. 75 | 76 | Args: 77 | account (AccountApi): AccountApi instance 78 | """ 79 | with patch("requests.get") as mock_request: 80 | mock_request.return_value.json.return_value = { 81 | "balances": self.mocked_data["balances"] 82 | } 83 | response = account.get_balances() 84 | assert response["balances"]["balanceType"] == "EUR" 85 | 86 | def test_get_details(self, account: AccountApi): 87 | """ 88 | Test get account details. 89 | 90 | Args: 91 | account (AccountApi): AccountApi instance 92 | """ 93 | with patch("requests.get") as mock_request: 94 | mock_request.return_value.json.return_value = self.mocked_data["details"] 95 | response = account.get_details() 96 | assert response["account"]["iban"] == self.iban 97 | 98 | def test_get_transactions(self, account: AccountApi, client: NordigenClient): 99 | """ 100 | Test get account transactions. 101 | 102 | Args: 103 | account (AccountApi): [description] 104 | """ 105 | with patch("requests.get") as mock_request: 106 | mock_request.return_value.json.return_value = { 107 | "transactions": self.mocked_data["transactions"] 108 | } 109 | response = account.get_transactions() 110 | 111 | assert ( 112 | response["transactions"]["booked"][0]["trxAmount"]["currency"] == "EUR" 113 | ) 114 | assert ( 115 | mock.call( 116 | url=f"{client.base_url}/accounts/{self.account_id}/transactions/", 117 | headers=client._headers, 118 | params={}, 119 | timeout = 10, 120 | ) 121 | in mock_request.call_args_list 122 | ) 123 | 124 | def test_get_premium_details(self, account: AccountApi, client): 125 | with patch("requests.get") as mock_request: 126 | mock_request.return_value.json.return_value = self.mocked_data["details"] 127 | response = account.get_premium_details(country="LV") 128 | assert response["account"]["iban"] == self.iban 129 | 130 | mock_request.assert_called_once_with( 131 | url=f"{client.base_url}/accounts/premium/{self.account_id}/details", 132 | headers=client._headers, 133 | params={"country": "LV"}, 134 | timeout = 10, 135 | ) 136 | 137 | def test_get_premium_transactions(self, account: AccountApi, client: NordigenClient): 138 | """ 139 | Test get premium transactions 140 | 141 | Args: 142 | account (AccountApi): AccountApi instance 143 | client (NordigenClient): NordigenClient instance 144 | """ 145 | with patch("requests.get") as mock_request: 146 | mock_request.return_value.json.return_value = { 147 | "transactions": self.mocked_data["transactions"] 148 | } 149 | account.get_premium_transactions( 150 | country="LV", 151 | date_from="2021-12-01", 152 | date_to="2022-01-21" 153 | ) 154 | mock_request.assert_called_once_with( 155 | url=f"{client.base_url}/accounts/premium/{self.account_id}/transactions", 156 | headers=client._headers, 157 | params={ 158 | "country": "LV", 159 | "date_from": "2021-12-01", 160 | "date_to": "2022-01-21", 161 | }, 162 | timeout = 10, 163 | ) 164 | -------------------------------------------------------------------------------- /nordigen/nordigen.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict, Final, Optional 3 | 4 | import requests 5 | from requests.models import HTTPError, Response 6 | 7 | from nordigen.api import ( 8 | AccountApi, 9 | AgreementsApi, 10 | InstitutionsApi, 11 | RequisitionsApi 12 | ) 13 | from nordigen.types.http_enums import HTTPMethod 14 | from nordigen.types.types import RequisitionDto, TokenType 15 | from nordigen.utils.filter import DataFilter 16 | 17 | 18 | class NordigenClient: 19 | """ 20 | Class to initialize new Client. 21 | 22 | Attributes 23 | --------- 24 | secret_key (str): Generated secret_key 25 | secret_id (str): Generated secret_id 26 | """ 27 | 28 | __ENDPOINT: Final = "token" 29 | 30 | def __init__( 31 | self, 32 | secret_key: str, 33 | secret_id: str, 34 | timeout: int = 10, 35 | base_url: str = "https://bankaccountdata.gocardless.com/api/v2" 36 | ) -> None: 37 | self.secret_key = secret_key 38 | self.secret_id = secret_id 39 | self.base_url = base_url 40 | self._headers = { 41 | "accept": "application/json", 42 | "Content-Type": "application/json", 43 | "User-Agent": "Nordigen-Python-v2", 44 | } 45 | self._token: Optional[str] = None 46 | self._timeout = timeout 47 | self.institution = InstitutionsApi(client=self) 48 | self.requisition = RequisitionsApi(client=self) 49 | self.agreement = AgreementsApi(client=self) 50 | self.data_filter = DataFilter() 51 | 52 | def account_api(self, id: str) -> AccountApi: 53 | """ 54 | Create Account api instance. 55 | 56 | Args: 57 | id (str): account id 58 | 59 | Returns: 60 | AccountApi: Account instance 61 | """ 62 | return AccountApi(client=self, id=id) 63 | 64 | @property 65 | def token(self): 66 | """ 67 | Get token. 68 | 69 | Returns: 70 | str: return token 71 | """ 72 | return self._token 73 | 74 | @token.setter 75 | def token(self, value: str): 76 | """ 77 | Set token. 78 | 79 | Args: 80 | value (str): token 81 | """ 82 | self._token = value 83 | self._headers["Authorization"] = f"Bearer {value}" 84 | 85 | def generate_token(self) -> TokenType: 86 | """ 87 | Generate new access token. 88 | 89 | Returns: 90 | TokenType: Dict that contains access and refresh token 91 | """ 92 | payload = {"secret_key": self.secret_key, "secret_id": self.secret_id} 93 | response = self.request( 94 | HTTPMethod.POST, 95 | f"{self.__ENDPOINT}/new/", 96 | payload, 97 | self._headers, 98 | ) 99 | 100 | self.token = response["access"] 101 | return response 102 | 103 | def exchange_token(self, refresh_token: str) -> TokenType: 104 | """ 105 | Exchange refresh token for access token. 106 | 107 | Args: 108 | refresh_token (str): refresh token 109 | 110 | Returns: 111 | TokenType: Dict that contains new access token 112 | """ 113 | payload = {"refresh": refresh_token} 114 | response = self.request( 115 | HTTPMethod.POST, 116 | f"{self.__ENDPOINT}/refresh/", 117 | payload, 118 | self._headers, 119 | ) 120 | 121 | self.token = response["access"] 122 | return response 123 | 124 | def request( 125 | self, 126 | method: HTTPMethod, 127 | endpoint: str, 128 | data: Dict = None, 129 | headers: Dict = None, 130 | ) -> Response: 131 | """ 132 | Request wrapper for Nordigen library. 133 | 134 | Args: 135 | method (HTTPMethod): Supports GET, POST, PUT, DELETE 136 | endpoint (str): [endpoint url 137 | data (Dict, optional): body or parameters that need to be sent alongside with the request. 138 | Defaults to {}. 139 | 140 | Raises: 141 | Exception: HTTP method is not supported 142 | HTTPError: HTTP error with status code 143 | 144 | Returns: 145 | Response: JSON Response object 146 | """ 147 | request_meta = { 148 | "url": f"{self.base_url}/{endpoint}", 149 | "headers": headers if headers else self._headers, 150 | } 151 | 152 | data = self.data_filter.filter_payload(data) 153 | 154 | if method == HTTPMethod.GET: 155 | response = requests.get(**request_meta, params=data, timeout=self._timeout) 156 | elif method == HTTPMethod.POST: 157 | response = requests.post(**request_meta, data=json.dumps(data), timeout=self._timeout) 158 | elif method == HTTPMethod.PUT: 159 | response = requests.put(**request_meta, data=json.dumps(data), timeout=self._timeout) 160 | elif method == HTTPMethod.DELETE: 161 | response = requests.delete(**request_meta, params=data, timeout=self._timeout) 162 | else: 163 | raise Exception(f'Method "{method}" is not supported') 164 | 165 | if response.ok: 166 | return response.json() 167 | 168 | raise HTTPError( 169 | {"response": response.json(), "status": response.status_code}, response=response 170 | ) 171 | 172 | def initialize_session( 173 | self, 174 | redirect_uri: str, 175 | institution_id: str, 176 | reference_id: str, 177 | max_historical_days: int = 90, 178 | access_valid_for_days: int = 90, 179 | account_selection: bool = False, 180 | ) -> RequisitionDto: 181 | """ 182 | Factory method that creates authorization in a specific institution 183 | and are responsible for the following steps: 184 | * Creates agreement 185 | * Creates requisition 186 | 187 | Returns: 188 | Dict[str]: link to initiate authorization with bank and requisition_id 189 | """ 190 | # Create agreement 191 | agreement = self.agreement.create_agreement( 192 | max_historical_days=max_historical_days, 193 | access_valid_for_days=access_valid_for_days, 194 | institution_id=institution_id, 195 | ) 196 | 197 | requisition_dict = { 198 | "redirect_uri": redirect_uri, 199 | "reference_id": reference_id, 200 | "institution_id": institution_id, 201 | "agreement": agreement["id"], 202 | "account_selection": account_selection, 203 | } 204 | 205 | # Create requisition 206 | requisition = self.requisition.create_requisition(**requisition_dict) 207 | 208 | return RequisitionDto( 209 | link=requisition["link"], requisition_id=requisition["id"] 210 | ) 211 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.2.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 22 | 23 | [[package]] 24 | name = "backports.entry-points-selectable" 25 | version = "1.1.0" 26 | description = "Compatibility shim providing selectable entry points for older implementations" 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7" 30 | 31 | [package.extras] 32 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 33 | testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 34 | 35 | [[package]] 36 | name = "black" 37 | version = "21.10b0" 38 | description = "The uncompromising code formatter." 39 | category = "dev" 40 | optional = false 41 | python-versions = ">=3.6.2" 42 | 43 | [package.dependencies] 44 | click = ">=7.1.2" 45 | mypy-extensions = ">=0.4.3" 46 | pathspec = ">=0.9.0,<1" 47 | platformdirs = ">=2" 48 | regex = ">=2020.1.8" 49 | tomli = ">=0.2.6,<2.0.0" 50 | typing-extensions = [ 51 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 52 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 53 | ] 54 | 55 | [package.extras] 56 | colorama = ["colorama (>=0.4.3)"] 57 | d = ["aiohttp (>=3.7.4)"] 58 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 59 | python2 = ["typed-ast (>=1.4.3)"] 60 | uvloop = ["uvloop (>=0.15.2)"] 61 | 62 | [[package]] 63 | name = "certifi" 64 | version = "2021.5.30" 65 | description = "Python package for providing Mozilla's CA Bundle." 66 | category = "main" 67 | optional = false 68 | python-versions = "*" 69 | 70 | [[package]] 71 | name = "cfgv" 72 | version = "3.3.1" 73 | description = "Validate configuration and produce human readable error messages." 74 | category = "dev" 75 | optional = false 76 | python-versions = ">=3.6.1" 77 | 78 | [[package]] 79 | name = "charset-normalizer" 80 | version = "2.0.4" 81 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 82 | category = "main" 83 | optional = false 84 | python-versions = ">=3.5.0" 85 | 86 | [package.extras] 87 | unicode_backport = ["unicodedata2"] 88 | 89 | [[package]] 90 | name = "click" 91 | version = "8.0.3" 92 | description = "Composable command line interface toolkit" 93 | category = "dev" 94 | optional = false 95 | python-versions = ">=3.6" 96 | 97 | [package.dependencies] 98 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 99 | 100 | [[package]] 101 | name = "colorama" 102 | version = "0.4.4" 103 | description = "Cross-platform colored terminal text." 104 | category = "dev" 105 | optional = false 106 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 107 | 108 | [[package]] 109 | name = "distlib" 110 | version = "0.3.3" 111 | description = "Distribution utilities" 112 | category = "dev" 113 | optional = false 114 | python-versions = "*" 115 | 116 | [[package]] 117 | name = "filelock" 118 | version = "3.3.2" 119 | description = "A platform independent file lock." 120 | category = "dev" 121 | optional = false 122 | python-versions = ">=3.6" 123 | 124 | [package.extras] 125 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 126 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 127 | 128 | [[package]] 129 | name = "identify" 130 | version = "2.3.3" 131 | description = "File identification library for Python" 132 | category = "dev" 133 | optional = false 134 | python-versions = ">=3.6.1" 135 | 136 | [package.extras] 137 | license = ["editdistance-s"] 138 | 139 | [[package]] 140 | name = "idna" 141 | version = "3.2" 142 | description = "Internationalized Domain Names in Applications (IDNA)" 143 | category = "main" 144 | optional = false 145 | python-versions = ">=3.5" 146 | 147 | [[package]] 148 | name = "iniconfig" 149 | version = "1.1.1" 150 | description = "iniconfig: brain-dead simple config-ini parsing" 151 | category = "dev" 152 | optional = false 153 | python-versions = "*" 154 | 155 | [[package]] 156 | name = "isort" 157 | version = "5.10.0" 158 | description = "A Python utility / library to sort Python imports." 159 | category = "dev" 160 | optional = false 161 | python-versions = ">=3.6.1,<4.0" 162 | 163 | [package.extras] 164 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 165 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 166 | colors = ["colorama (>=0.4.3,<0.5.0)"] 167 | plugins = ["setuptools"] 168 | 169 | [[package]] 170 | name = "mypy-extensions" 171 | version = "0.4.3" 172 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 173 | category = "dev" 174 | optional = false 175 | python-versions = "*" 176 | 177 | [[package]] 178 | name = "nodeenv" 179 | version = "1.6.0" 180 | description = "Node.js virtual environment builder" 181 | category = "dev" 182 | optional = false 183 | python-versions = "*" 184 | 185 | [[package]] 186 | name = "packaging" 187 | version = "21.0" 188 | description = "Core utilities for Python packages" 189 | category = "dev" 190 | optional = false 191 | python-versions = ">=3.6" 192 | 193 | [package.dependencies] 194 | pyparsing = ">=2.0.2" 195 | 196 | [[package]] 197 | name = "pathspec" 198 | version = "0.9.0" 199 | description = "Utility library for gitignore style pattern matching of file paths." 200 | category = "dev" 201 | optional = false 202 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 203 | 204 | [[package]] 205 | name = "platformdirs" 206 | version = "2.4.0" 207 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 208 | category = "dev" 209 | optional = false 210 | python-versions = ">=3.6" 211 | 212 | [package.extras] 213 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 214 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 215 | 216 | [[package]] 217 | name = "pluggy" 218 | version = "1.0.0" 219 | description = "plugin and hook calling mechanisms for python" 220 | category = "dev" 221 | optional = false 222 | python-versions = ">=3.6" 223 | 224 | [package.extras] 225 | dev = ["pre-commit", "tox"] 226 | testing = ["pytest", "pytest-benchmark"] 227 | 228 | [[package]] 229 | name = "pre-commit" 230 | version = "2.15.0" 231 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 232 | category = "dev" 233 | optional = false 234 | python-versions = ">=3.6.1" 235 | 236 | [package.dependencies] 237 | cfgv = ">=2.0.0" 238 | identify = ">=1.0.0" 239 | nodeenv = ">=0.11.1" 240 | pyyaml = ">=5.1" 241 | toml = "*" 242 | virtualenv = ">=20.0.8" 243 | 244 | [[package]] 245 | name = "py" 246 | version = "1.10.0" 247 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 248 | category = "dev" 249 | optional = false 250 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 251 | 252 | [[package]] 253 | name = "pyparsing" 254 | version = "2.4.7" 255 | description = "Python parsing module" 256 | category = "dev" 257 | optional = false 258 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 259 | 260 | [[package]] 261 | name = "pytest" 262 | version = "6.2.5" 263 | description = "pytest: simple powerful testing with Python" 264 | category = "dev" 265 | optional = false 266 | python-versions = ">=3.6" 267 | 268 | [package.dependencies] 269 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 270 | attrs = ">=19.2.0" 271 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 272 | iniconfig = "*" 273 | packaging = "*" 274 | pluggy = ">=0.12,<2.0" 275 | py = ">=1.8.2" 276 | toml = "*" 277 | 278 | [package.extras] 279 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 280 | 281 | [[package]] 282 | name = "python-dotenv" 283 | version = "0.19.2" 284 | description = "Read key-value pairs from a .env file and set them as environment variables" 285 | category = "dev" 286 | optional = false 287 | python-versions = ">=3.5" 288 | 289 | [package.extras] 290 | cli = ["click (>=5.0)"] 291 | 292 | [[package]] 293 | name = "pyyaml" 294 | version = "6.0" 295 | description = "YAML parser and emitter for Python" 296 | category = "dev" 297 | optional = false 298 | python-versions = ">=3.6" 299 | 300 | [[package]] 301 | name = "regex" 302 | version = "2021.11.2" 303 | description = "Alternative regular expression module, to replace re." 304 | category = "dev" 305 | optional = false 306 | python-versions = "*" 307 | 308 | [[package]] 309 | name = "requests" 310 | version = "2.26.0" 311 | description = "Python HTTP for Humans." 312 | category = "main" 313 | optional = false 314 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 315 | 316 | [package.dependencies] 317 | certifi = ">=2017.4.17" 318 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 319 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 320 | urllib3 = ">=1.21.1,<1.27" 321 | 322 | [package.extras] 323 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 324 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 325 | 326 | [[package]] 327 | name = "six" 328 | version = "1.16.0" 329 | description = "Python 2 and 3 compatibility utilities" 330 | category = "dev" 331 | optional = false 332 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 333 | 334 | [[package]] 335 | name = "toml" 336 | version = "0.10.2" 337 | description = "Python Library for Tom's Obvious, Minimal Language" 338 | category = "dev" 339 | optional = false 340 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 341 | 342 | [[package]] 343 | name = "tomli" 344 | version = "1.2.2" 345 | description = "A lil' TOML parser" 346 | category = "dev" 347 | optional = false 348 | python-versions = ">=3.6" 349 | 350 | [[package]] 351 | name = "typing-extensions" 352 | version = "3.10.0.2" 353 | description = "Backported and Experimental Type Hints for Python 3.5+" 354 | category = "dev" 355 | optional = false 356 | python-versions = "*" 357 | 358 | [[package]] 359 | name = "urllib3" 360 | version = "1.26.6" 361 | description = "HTTP library with thread-safe connection pooling, file post, and more." 362 | category = "main" 363 | optional = false 364 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 365 | 366 | [package.extras] 367 | brotli = ["brotlipy (>=0.6.0)"] 368 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 369 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 370 | 371 | [[package]] 372 | name = "virtualenv" 373 | version = "20.10.0" 374 | description = "Virtual Python Environment builder" 375 | category = "dev" 376 | optional = false 377 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 378 | 379 | [package.dependencies] 380 | "backports.entry-points-selectable" = ">=1.0.4" 381 | distlib = ">=0.3.1,<1" 382 | filelock = ">=3.2,<4" 383 | platformdirs = ">=2,<3" 384 | six = ">=1.9.0,<2" 385 | 386 | [package.extras] 387 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 388 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 389 | 390 | [metadata] 391 | lock-version = "1.1" 392 | python-versions = "^3.8" 393 | content-hash = "faade7b6add2f9ec56e4ca87dca95b308f0cf98e91e811b53858d5ddfb34013a" 394 | 395 | [metadata.files] 396 | atomicwrites = [ 397 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 398 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 399 | ] 400 | attrs = [ 401 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 402 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 403 | ] 404 | "backports.entry-points-selectable" = [ 405 | {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, 406 | {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, 407 | ] 408 | black = [ 409 | {file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"}, 410 | {file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"}, 411 | ] 412 | certifi = [ 413 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 414 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 415 | ] 416 | cfgv = [ 417 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 418 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 419 | ] 420 | charset-normalizer = [ 421 | {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, 422 | {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, 423 | ] 424 | click = [ 425 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 426 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 427 | ] 428 | colorama = [ 429 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 430 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 431 | ] 432 | distlib = [ 433 | {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, 434 | {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, 435 | ] 436 | filelock = [ 437 | {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, 438 | {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, 439 | ] 440 | identify = [ 441 | {file = "identify-2.3.3-py2.py3-none-any.whl", hash = "sha256:ffab539d9121b386ffdea84628ff3eefda15f520f392ce11b393b0a909632cdf"}, 442 | {file = "identify-2.3.3.tar.gz", hash = "sha256:b9ffbeb7ed87e96ce017c66b80ca04fda3adbceb5c74e54fc7d99281d27d0859"}, 443 | ] 444 | idna = [ 445 | {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, 446 | {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, 447 | ] 448 | iniconfig = [ 449 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 450 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 451 | ] 452 | isort = [ 453 | {file = "isort-5.10.0-py3-none-any.whl", hash = "sha256:1a18ccace2ed8910bd9458b74a3ecbafd7b2f581301b0ab65cfdd4338272d76f"}, 454 | {file = "isort-5.10.0.tar.gz", hash = "sha256:e52ff6d38012b131628cf0f26c51e7bd3a7c81592eefe3ac71411e692f1b9345"}, 455 | ] 456 | mypy-extensions = [ 457 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 458 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 459 | ] 460 | nodeenv = [ 461 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 462 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 463 | ] 464 | packaging = [ 465 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 466 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 467 | ] 468 | pathspec = [ 469 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 470 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 471 | ] 472 | platformdirs = [ 473 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 474 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 475 | ] 476 | pluggy = [ 477 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 478 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 479 | ] 480 | pre-commit = [ 481 | {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, 482 | {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, 483 | ] 484 | py = [ 485 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 486 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 487 | ] 488 | pyparsing = [ 489 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 490 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 491 | ] 492 | pytest = [ 493 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 494 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 495 | ] 496 | python-dotenv = [ 497 | {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, 498 | {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, 499 | ] 500 | pyyaml = [ 501 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 502 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 503 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 504 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 505 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 506 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 507 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 508 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 509 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 510 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 511 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 512 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 513 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 514 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 515 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 516 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 517 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 518 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 519 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 520 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 521 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 522 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 523 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 524 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 525 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 526 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 527 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 528 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 529 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 530 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 531 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 532 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 533 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 534 | ] 535 | regex = [ 536 | {file = "regex-2021.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:897c539f0f3b2c3a715be651322bef2167de1cdc276b3f370ae81a3bda62df71"}, 537 | {file = "regex-2021.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:886f459db10c0f9d17c87d6594e77be915f18d343ee138e68d259eb385f044a8"}, 538 | {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075b0fdbaea81afcac5a39a0d1bb91de887dd0d93bf692a5dd69c430e7fc58cb"}, 539 | {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6238d30dcff141de076344cf7f52468de61729c2f70d776fce12f55fe8df790"}, 540 | {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fab29411d75c2eb48070020a40f80255936d7c31357b086e5931c107d48306e"}, 541 | {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0148988af0182a0a4e5020e7c168014f2c55a16d11179610f7883dd48ac0ebe"}, 542 | {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be30cd315db0168063a1755fa20a31119da91afa51da2907553493516e165640"}, 543 | {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e9cec3a62d146e8e122d159ab93ac32c988e2ec0dcb1e18e9e53ff2da4fbd30c"}, 544 | {file = "regex-2021.11.2-cp310-cp310-win32.whl", hash = "sha256:41c66bd6750237a8ed23028a6c9173dc0c92dc24c473e771d3bfb9ee817700c3"}, 545 | {file = "regex-2021.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:0075fe4e2c2720a685fef0f863edd67740ff78c342cf20b2a79bc19388edf5db"}, 546 | {file = "regex-2021.11.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ed3465acf8c7c10aa2e0f3d9671da410ead63b38a77283ef464cbb64275df58"}, 547 | {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab1fea8832976ad0bebb11f652b692c328043057d35e9ebc78ab0a7a30cf9a70"}, 548 | {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1e44d860345ab5d4f533b6c37565a22f403277f44c4d2d5e06c325da959883"}, 549 | {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9486ebda015913909bc28763c6b92fcc3b5e5a67dee4674bceed112109f5dfb8"}, 550 | {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20605bfad484e1341b2cbfea0708e4b211d233716604846baa54b94821f487cb"}, 551 | {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f20f9f430c33597887ba9bd76635476928e76cad2981643ca8be277b8e97aa96"}, 552 | {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d85ca137756d62c8138c971453cafe64741adad1f6a7e63a22a5a8abdbd19fa"}, 553 | {file = "regex-2021.11.2-cp36-cp36m-win32.whl", hash = "sha256:af23b9ca9a874ef0ec20e44467b8edd556c37b0f46f93abfa93752ea7c0e8d1e"}, 554 | {file = "regex-2021.11.2-cp36-cp36m-win_amd64.whl", hash = "sha256:070336382ca92c16c45b4066c4ba9fa83fb0bd13d5553a82e07d344df8d58a84"}, 555 | {file = "regex-2021.11.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef4e53e2fdc997d91f5b682f81f7dc9661db9a437acce28745d765d251902d85"}, 556 | {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35ed5714467fc606551db26f80ee5d6aa1f01185586a7bccd96f179c4b974a11"}, 557 | {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee36d5113b6506b97f45f2e8447cb9af146e60e3f527d93013d19f6d0405f3b"}, 558 | {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fba661a4966adbd2c3c08d3caad6822ecb6878f5456588e2475ae23a6e47929"}, 559 | {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f9d16f7970791f17ecce7e7f101548314ed1ee2583d4268601f30af3170856"}, 560 | {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6a28e87ba69f3a4f30d775b179aac55be1ce59f55799328a0d9b6df8f16b39d"}, 561 | {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9267e4fba27e6dd1008c4f2983cc548c98b4be4444e3e342db11296c0f45512f"}, 562 | {file = "regex-2021.11.2-cp37-cp37m-win32.whl", hash = "sha256:d4bfe3bc3976ccaeb4ae32f51e631964e2f0e85b2b752721b7a02de5ce3b7f27"}, 563 | {file = "regex-2021.11.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2bb7cae741de1aa03e3dd3a7d98c304871eb155921ca1f0d7cc11f5aade913fd"}, 564 | {file = "regex-2021.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:23f93e74409c210de4de270d4bf88fb8ab736a7400f74210df63a93728cf70d6"}, 565 | {file = "regex-2021.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8ee91e1c295beb5c132ebd78616814de26fedba6aa8687ea460c7f5eb289b72"}, 566 | {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3ff69ab203b54ce5c480c3ccbe959394ea5beef6bd5ad1785457df7acea92e"}, 567 | {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3c00cb5c71da655e1e5161481455479b613d500dd1bd252aa01df4f037c641f"}, 568 | {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf35e16f4b639daaf05a2602c1b1d47370e01babf9821306aa138924e3fe92"}, 569 | {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb11c982a849dc22782210b01d0c1b98eb3696ce655d58a54180774e4880ac66"}, 570 | {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e3755e0f070bc31567dfe447a02011bfa8444239b3e9e5cca6773a22133839"}, 571 | {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0621c90f28d17260b41838b22c81a79ff436141b322960eb49c7b3f91d1cbab6"}, 572 | {file = "regex-2021.11.2-cp38-cp38-win32.whl", hash = "sha256:8fbe1768feafd3d0156556677b8ff234c7bf94a8110e906b2d73506f577a3269"}, 573 | {file = "regex-2021.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9ee98d658a146cb6507be720a0ce1b44f2abef8fb43c2859791d91aace17cd5"}, 574 | {file = "regex-2021.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3794cea825f101fe0df9af8a00f9fad8e119c91e39a28636b95ee2b45b6c2e5"}, 575 | {file = "regex-2021.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3576e173e7b4f88f683b4de7db0c2af1b209bb48b2bf1c827a6f3564fad59a97"}, 576 | {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4f4810117a9072a5aa70f7fea5f86fa9efbe9a798312e0a05044bd707cc33"}, 577 | {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5930d334c2f607711d54761956aedf8137f83f1b764b9640be21d25a976f3a4"}, 578 | {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:956187ff49db7014ceb31e88fcacf4cf63371e6e44d209cf8816cd4a2d61e11a"}, 579 | {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e095f7f96a4b9f24b93c2c915f31a5201a6316618d919b0593afb070a5270e"}, 580 | {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56735c35a3704603d9d7b243ee06139f0837bcac2171d9ba1d638ce1df0742a"}, 581 | {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adf35d88d9cffc202e6046e4c32e1e11a1d0238b2fcf095c94f109e510ececea"}, 582 | {file = "regex-2021.11.2-cp39-cp39-win32.whl", hash = "sha256:30fe317332de0e50195665bc61a27d46e903d682f94042c36b3f88cb84bd7958"}, 583 | {file = "regex-2021.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:85289c25f658e3260b00178757c87f033f3d4b3e40aa4abdd4dc875ff11a94fb"}, 584 | {file = "regex-2021.11.2.tar.gz", hash = "sha256:5e85dcfc5d0f374955015ae12c08365b565c6f1eaf36dd182476a4d8e5a1cdb7"}, 585 | ] 586 | requests = [ 587 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 588 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 589 | ] 590 | six = [ 591 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 592 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 593 | ] 594 | toml = [ 595 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 596 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 597 | ] 598 | tomli = [ 599 | {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, 600 | {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, 601 | ] 602 | typing-extensions = [ 603 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 604 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 605 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 606 | ] 607 | urllib3 = [ 608 | {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, 609 | {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, 610 | ] 611 | virtualenv = [ 612 | {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, 613 | {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, 614 | ] 615 | --------------------------------------------------------------------------------