├── .github ├── CODEOWNERS └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── Makefile ├── README.md ├── requirements ├── ci.txt └── prod.txt ├── setup.cfg ├── setup.py ├── src └── python_lei │ ├── __init__.py │ ├── exceptions.py │ ├── isin_lei.py │ ├── lei_search.py │ ├── parser.py │ ├── pylei.py │ └── utils.py └── tests ├── __init__.py ├── assets ├── dataframe_getleiinfo.csv ├── isin_response.csv └── test_lei_isin.csv ├── test_lei_search.py └── test_pylei.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @jdvala -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python LEI 2 | 3 | on: 4 | pull_request: 5 | branches: '*' 6 | push: 7 | branches: ["master"] 8 | tags: ["*"] 9 | jobs: 10 | CI: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] # Specify Python versions here 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install -r requirements/ci.txt 27 | pip install -e . 28 | 29 | - name: Download Data 30 | run: python -c "from python_lei.utils import Download; Download(_is_actions=True)" 31 | 32 | - name: Test 33 | run: python -m pytest tests/ 34 | 35 | - name: Codecov 36 | env: 37 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 38 | run: | 39 | pip install codecov 40 | codecov 41 | 42 | CD: 43 | needs: CI 44 | if: startsWith(github.ref, 'refs/tags/') 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Set up Python 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: 3.x 53 | 54 | - name: PyPi Deploy preparation 55 | run: | 56 | pip install --upgrade setuptools wheel 57 | python setup.py sdist bdist_wheel --universal 58 | - name: PyPi Deploy 59 | uses: pypa/gh-action-pypi-publish@v1.0.0a0 60 | with: 61 | user: ${{ secrets.PYPI_USER }} 62 | password: ${{ secrets.PYPI_PASSWORD }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary and binary files 2 | *~ 3 | *.py[cod] 4 | *.so 5 | *.cfg 6 | !.isort.cfg 7 | !setup.cfg 8 | *.orig 9 | *.log 10 | *.pot 11 | __pycache__/* 12 | .cache/* 13 | .*.swp 14 | */.ipynb_checkpoints/* 15 | .DS_Store 16 | 17 | # Project files 18 | .ropeproject 19 | .project 20 | .pydevproject 21 | .settings 22 | .idea 23 | tags 24 | 25 | # Package files 26 | *.egg 27 | *.eggs/ 28 | .installed.cfg 29 | *.egg-info 30 | 31 | # Unittest and coverage 32 | htmlcov/* 33 | .coverage 34 | .tox 35 | junit.xml 36 | coverage.xml 37 | .pytest_cache/ 38 | 39 | # Build and docs folder/files 40 | build/* 41 | dist/* 42 | sdist/* 43 | docs/api/* 44 | docs/_rst/* 45 | docs/_build/* 46 | cover/* 47 | MANIFEST 48 | 49 | # Per-project virtualenvs 50 | .venv*/ 51 | 52 | # Resources 53 | resources/* 54 | 55 | # Vscode 56 | .vscode -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jdvala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude * 2 | include setup.py 3 | include README* 4 | include LICENSE* 5 | graft src 6 | graft requirements 7 | exclude requirements/ci.txt 8 | recursive-exclude * __pycache__ -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | find src -type d -name "__pycache__" -exec rm -rf {} + > /dev/null 2>&1 3 | find src -type f -name "*.pyc" -exec rm -rf {} + > /dev/null 2>&1 4 | 5 | find tests -type d -name "__pycache__" -exec rm -rf {} + > /dev/null 2>&1 6 | find tests -type f -name "*.pyc" -exec rm -rf {} + > /dev/null 2>&1 7 | 8 | lint: 9 | flake8 --show-source src 10 | isort --check-only -rc src --diff 11 | 12 | flake8 --show-source tests 13 | isort --check-only -rc tests --diff 14 | 15 | flake8 --show-source setup.py 16 | isort --check-only setup.py --diff 17 | 18 | test: 19 | python -m pytest tests/ 20 | 21 | install: 22 | pip install -r requirements/ci.txt 23 | pip install -e . 24 | 25 | all: clean lint test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-lei 2 | 3 | [![python](https://img.shields.io/pypi/pyversions/python-lei?logo=python&logoColor=white&style=plastic)](https://www.python.org) 4 | [![codecov](https://codecov.io/gh/jdvala/python-lei/branch/master/graph/badge.svg)](https://codecov.io/gh/jdvala/python-lei) 5 | [![black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) 6 | ![Python LEI](https://github.com/jdvala/python-lei/workflows/Python%20LEI/badge.svg) 7 | [![pypi Version](https://img.shields.io/pypi/v/python-lei.svg?logo=pypi&logoColor=white)](https://pypi.org/project/python-lei/) 8 | [![downloads](https://pepy.tech/badge/python-lei)](https://pepy.tech/project/python-lei) 9 | 10 | 11 | This project is wraper for Leilex, legal entity identifier API. Includes ISIN-LEI conversion. Search LEI number using company name. 12 | 13 | ## Dependencies 14 | 15 | 1. [Python](https://www.python.org/) >= 3.4 16 | 2. [requests](http://docs.python-requests.org/en/master/) 17 | 3. [dateutils](https://dateutil.readthedocs.io/en/stable/) 18 | 4. [pandas](https://pandas.pydata.org/) 19 | 20 | ## Installation 21 | ```bash 22 | pip install python-lei 23 | ``` 24 | 25 | ## Usage 26 | 27 | After installing the module, first step is to download the data for ISIN and LEI mappings 28 | 29 | ```python 30 | >>> from python_lei.utils import Download 31 | 32 | >>> Download() 33 | ``` 34 | 35 | This will download latest ISIN LEI mappings into resources directory. This is only necessary if you want to use ISIN LEI conversion. 36 | 37 | ### Get LEI information 38 | 39 | ```python 40 | >>> from python_lei.pylei import pyLEI 41 | 42 | >>> getinfo = pyLEI() 43 | 44 | >>> raw_output, lei_results, dataframe = getinfo.get_lei_info(["A23RUXWKASG834LTMK28"], return_dataframe=True) 45 | 46 | >>> print(raw_output) 47 | 48 | [{'total_record_count': 1, 49 | 'page_number': 1, 50 | 'page_size': 100, 51 | 'total_pages': 1, 52 | 'has_more': False, 53 | 'records': [{'LEI': 'A23RUXWKASG834LTMK28', 54 | 'LegalName': 'AUTOLIV, INC.', 55 | 'LegalJurisdiction': 'US-DE', 56 | 'LegalForm': 'XTIQ', 57 | 'OtherLegalForm': '', 58 | 'EntityStatus': 'ACTIVE', 59 | 'EntityExpirationDate': None, 60 | 'EntityExpirationReason': '', 61 | 'SuccessorEntity': '', 62 | 'InitialRegistrationDate': '2012-06-06T03:52:00.000 +00:00', 63 | 'LastUpdateDate': '2019-12-18T03:32:00.000 +00:00', 64 | 'RegistrationStatus': 'ISSUED', 65 | 'NextRenewalDate': '2020-12-15T10:15:00.000 +00:00', 66 | 'ManagingLOU': 'EVK05KS7XY1DEII3R011', 67 | 'ValidationSources': 'FULLY_CORROBORATED', 68 | 'AssociatedLEI': '', 69 | 'AssociatedEntityName': '', 70 | 'AssociatedEntityType': '', 71 | 'RegistrationAuthorityID': 'RA000602 ', 72 | 'OtherRegistrationAuthorityID': '', 73 | 'RegistrationAuthorityEntityID': '2155072', 74 | 'EntityCategory': '', 75 | 'Addresses': [{'Line1': 'Box 70381', 76 | 'Line2': '', 77 | 'Line3': '', 78 | 'Line4': '', 79 | 'City': 'Stockholm', 80 | 'Region': 'SE-AB', 81 | 'Country': 'SE', 82 | 'PostalCode': '107 24', 83 | 'OtherType': '', 84 | 'AddressType': 'HEADQUARTERS_ADDRESS'}, 85 | {'Line1': 'C/O THE CORPORATION TRUST COMPANY', 86 | 'Line2': 'CORPORATION TRUST CENTER 1209 ORANGE ST', 87 | 'Line3': '', 88 | 'Line4': '', 89 | 'City': 'WILMINGTON', 90 | 'Region': 'US-DE', 91 | 'Country': 'US', 92 | 'PostalCode': '19801', 93 | 'OtherType': '', 94 | 'AddressType': 'LEGAL_ADDRESS'}], 95 | 'OtherNames': [], 96 | 'ValidationAuthorities': [{'ValidationAuthorityID': 'RA000602', 97 | 'OtherValidationAuthorityID': '', 98 | 'ValidationAuthorityEntityID': '2155072'}], 99 | 'Relationships': [], 100 | 'ReportingExceptions': [{'LEI': 'A23RUXWKASG834LTMK28', 101 | 'ExceptionCategory': 'DIRECT_ACCOUNTING_CONSOLIDATION_PARENT', 102 | 'ExceptionReasons': [{'Reason': 'NON_CONSOLIDATING'}], 103 | 'ExceptionReferences': []}, 104 | {'LEI': 'A23RUXWKASG834LTMK28', 105 | 'ExceptionCategory': 'ULTIMATE_ACCOUNTING_CONSOLIDATION_PARENT', 106 | 'ExceptionReasons': [{'Reason': 'NON_CONSOLIDATING'}], 107 | 'ExceptionReferences': []}]}]}] 108 | 109 | 110 | # Class based retrieval 111 | >>> print(lei_results.lei_names) 112 | ['AUTOLIV, INC.'] 113 | 114 | >>> print(lei_results.lei_list) 115 | ['A23RUXWKASG834LTMK28'] 116 | 117 | # Dataframe 118 | >>> print(dataframe[["LEI", "Legal_Name"]]) 119 | | | LEI | Legal_Name | 120 | |---:|:---------------------|:--------------| 121 | | 0 | A23RUXWKASG834LTMK28 | AUTOLIV, INC. | 122 | ``` 123 | 124 | ### Get LEI-ISIN information 125 | 126 | ```python 127 | # LEI TO ISIN 128 | >>> from python_lei.isin_lei import ISINtoLEI, LEItoISIN 129 | 130 | >>> lei_to_isin = LEItoISIN() 131 | 132 | >>> isin_list, dataframe = lei_to_isin.get_isin("A23RUXWKASG834LTMK28", return_dataframe=True) 133 | 134 | >>> print(isin_list) 135 | ['SE0000382335', 136 | 'US052800AB59', 137 | 'US0528002084', 138 | 'US0528003074', 139 | 'US0528001094', 140 | 'US0528001177'] 141 | 142 | >>> print(dataframe) 143 | | | LEI | ISIN | 144 | |--------:|:---------------------|:-------------| 145 | | 1858574 | A23RUXWKASG834LTMK28 | SE0000382335 | 146 | | 2141681 | A23RUXWKASG834LTMK28 | US052800AB59 | 147 | | 2990824 | A23RUXWKASG834LTMK28 | US0528002084 | 148 | | 3450877 | A23RUXWKASG834LTMK28 | US0528003074 | 149 | | 3766379 | A23RUXWKASG834LTMK28 | US0528001094 | 150 | | 4442500 | A23RUXWKASG834LTMK28 | US0528001177 | 151 | 152 | # ISIN TO LEI 153 | >> isin_to_lei = ISINtoLEI() 154 | 155 | >> lei_number = isin_to_lei.get_lei("US0528003074") 156 | 157 | >> print(lei_number) 158 | ['A23RUXWKASG834LTMK28'] 159 | ``` 160 | 161 | ### Search LEI using company name 162 | 163 | You can also search for possible LEI numbers for a given company name. 164 | 165 | ```python 166 | >>> from python_lei.lei_search import SearchLEI 167 | >>> search_possible_lei = SearchLEI() 168 | >>> raw_data, table = search_possible_lei.search_lei("Apple INC.", show_table=True) 169 | >>> print(table) 170 | +------------+----------------------+ 171 | | Legal Name | LEI | 172 | +------------+----------------------+ 173 | | APPLE INC. | HWUPKR0MPOU8FGXBT394 | 174 | +------------+----------------------+ 175 | 176 | >>> print(raw_data) 177 | [{'LegalName': 'APPLE INC.', 'LEI': 'HWUPKR0MPOU8FGXBT394'}] 178 | -------------------------------------------------------------------------------- /requirements/ci.txt: -------------------------------------------------------------------------------- 1 | black 2 | isort 3 | flake8 4 | flake8-absolute-import 5 | flake8-black 6 | flake8-blind-except 7 | flake8-comprehensions 8 | flake8-docstrings 9 | flake8-mutable 10 | flake8-print 11 | flake8-quotes 12 | flake8-tuple 13 | pytest 14 | pytest-cov 15 | pytest-env 16 | pytest-sugar 17 | testfixtures 18 | -------------------------------------------------------------------------------- /requirements/prod.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | texttable 3 | requests 4 | bs4 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = -s --strict -vv --cache-clear --maxfail=1 --cov=python_lei --cov-report=term --cov-report=html --cov-branch --no-cov-on-fail 3 | 4 | [isort] 5 | multi_line_output = 3 6 | not_skip = __init__.py 7 | include_trailing_comma = True 8 | force_grid_wrap = 0 9 | use_parentheses = True 10 | line_length = 88 11 | default_section = THIRDPARTY 12 | known_first_party = python-lei,tests 13 | 14 | [flake8] 15 | ignore = D10,E203,E501,W503 16 | max-line-length = 88 17 | select = A,B,C4,D,E,F,M,Q,T,W,ABS,BLK 18 | inline-quotes = " 19 | 20 | [coverage:run] 21 | branch = True 22 | omit = site-packages 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os.path as path 2 | 3 | from setuptools import find_packages, setup 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | requirements_path = path.join(here, "requirements", "prod.txt") 8 | 9 | readme_path = path.join(here, "README.md") 10 | 11 | 12 | def read_requirements(path): 13 | try: 14 | with open(path, mode="rt", encoding="utf-8") as fp: 15 | return list( 16 | filter(bool, [line.split("#")[0].strip() for line in fp]) # noqa:C407 17 | ) 18 | except IndexError: 19 | raise RuntimeError("{} is broken".format(path)) 20 | 21 | 22 | def read_readme(path): 23 | with open(path, mode="rt", encoding="utf-8") as fp: 24 | return fp.read() 25 | 26 | 27 | setup( 28 | name="python-lei", 29 | description="Python wraper for Legal Entity Identification API and ISIN to LEI and vice versa conversion.", 30 | version="0.1.1", 31 | long_description=read_readme(readme_path), 32 | long_description_content_type="text/markdown", 33 | install_requires=read_requirements(requirements_path), 34 | include_package_data=True, 35 | package_data={}, 36 | packages=find_packages(where="src"), 37 | package_dir={"": "src"}, 38 | author="jdvala", 39 | author_email="jay.vala@msn.com", 40 | url="https://github.com/jdvala/python-lei", 41 | python_requires=">=3.5", 42 | classifiers=[ 43 | "Development Status :: 5 - Production/Stable", 44 | "Intended Audience :: Developers", 45 | "License :: OSI Approved :: MIT License", 46 | "Operating System :: OS Independent", 47 | "Programming Language :: Python :: 3.5", 48 | "Programming Language :: Python :: 3.6", 49 | "Programming Language :: Python :: 3.7", 50 | "Programming Language :: Python :: 3.8", 51 | "Topic :: Software Development :: Libraries :: Python Modules", 52 | "Topic :: Utilities", 53 | ], 54 | keywords="LEI python-lei Legal-Entity-Identification ISIN Finance Business LEI-code Leilex", 55 | license="MIT", 56 | ) 57 | -------------------------------------------------------------------------------- /src/python_lei/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from pkg_resources import DistributionNotFound, get_distribution 5 | from python_lei import pylei 6 | 7 | try: 8 | # Change here if project is renamed and does not equal the package name 9 | dist_name = "python-lei" 10 | __version__ = get_distribution(dist_name).version 11 | except DistributionNotFound: 12 | __version__ = "unknown" 13 | finally: 14 | del get_distribution, DistributionNotFound 15 | 16 | 17 | logging.basicConfig(level=logging.INFO) 18 | logger = logging.getLogger() 19 | -------------------------------------------------------------------------------- /src/python_lei/exceptions.py: -------------------------------------------------------------------------------- 1 | class RecordNotFound(Exception): 2 | pass 3 | 4 | 5 | class InvalidLEI(Exception): 6 | pass 7 | 8 | 9 | class InvalidISIN(Exception): 10 | pass 11 | 12 | 13 | class NotFound(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /src/python_lei/isin_lei.py: -------------------------------------------------------------------------------- 1 | from python_lei.exceptions import InvalidISIN, InvalidLEI 2 | from python_lei.utils import load_data 3 | 4 | 5 | class LEItoISIN: 6 | """ 7 | Get ISIN from corresponding LEI 8 | """ 9 | 10 | def __init__(self): 11 | self.dataframe = load_data() 12 | 13 | def get_isin(self, lei, return_dataframe=False): 14 | """ 15 | Get all the possible ISIN numbers for a LEI 16 | """ 17 | if len(lei) != 20: 18 | raise InvalidLEI("Invalid LEI number") 19 | 20 | isin_dataframe = self.dataframe[self.dataframe["LEI"] == lei] 21 | 22 | isin_list = isin_dataframe["ISIN"].tolist() 23 | 24 | if return_dataframe: 25 | return isin_list, isin_dataframe 26 | 27 | return isin_list 28 | 29 | 30 | class ISINtoLEI: 31 | """ 32 | Get LEI from corresponding ISIN 33 | """ 34 | 35 | def __init__(self): 36 | self.dataframe = load_data() 37 | 38 | def get_lei(self, isin): 39 | """ 40 | Get all the possible ISIN numbers for a LEI 41 | """ 42 | if len(isin) != 12: 43 | raise InvalidISIN("Invalid ISIN number") 44 | 45 | lei_dataframe = self.dataframe[self.dataframe["ISIN"] == isin] 46 | 47 | lei_list = lei_dataframe["LEI"].tolist() 48 | 49 | return lei_list 50 | -------------------------------------------------------------------------------- /src/python_lei/lei_search.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from python_lei.exceptions import NotFound 3 | import logging 4 | from texttable import Texttable 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class SearchLEI: 11 | """ 12 | Search LEI number from name 13 | """ 14 | 15 | def __init__(self): 16 | self.search_url = "https://api.leilex.com/API/LEI/AutoComplete/" 17 | 18 | def search_lei(self, company_name, show_table=False): 19 | """ 20 | Search LEI numbers with company name 21 | 22 | Args: 23 | company_name (str): Name of the company to search LEI numbers for 24 | 25 | Returns: 26 | raw_output (List): Raw output from the API 27 | 28 | """ 29 | if not isinstance(company_name, str): 30 | raise ValueError("Company Name should be a string") 31 | 32 | if not company_name: 33 | raise NotFound("Company Name not found. Please provide a company name") 34 | 35 | raw_output = self._request_api(company_name) 36 | 37 | if not raw_output: 38 | logger.info(f"No LEI found for company name {company_name}") 39 | return 40 | 41 | if not show_table: 42 | return raw_output 43 | 44 | # Initalize the table 45 | table = Texttable() 46 | table.add_row(["Legal Name", "LEI"]) 47 | 48 | for legal_entity in raw_output: 49 | table.add_row([legal_entity.get("LegalName"), legal_entity.get("LEI")]) 50 | 51 | return raw_output, table.draw() 52 | 53 | def _request_api(self, company_name): 54 | """ 55 | Request LEI autocomplete api to obtain possible list of LEI numbers 56 | """ 57 | try: 58 | response = requests.get( 59 | f"{self.search_url}?query={company_name}&filterType=Name" 60 | ) 61 | 62 | if not response.ok: 63 | logger.info("No response form the API") 64 | return 65 | return response.json() 66 | except requests.exceptions.ConnectionError as err: 67 | logger.error("Error connecting to API host", exc_info=err) 68 | return 69 | -------------------------------------------------------------------------------- /src/python_lei/parser.py: -------------------------------------------------------------------------------- 1 | from python_lei.exceptions import RecordNotFound 2 | 3 | 4 | class LEIParser: 5 | """ 6 | Helper class to parse LEI response returned from the API 7 | """ 8 | 9 | def __init__(self, lei_responses): 10 | """ 11 | Get the output from the API and assign them to variables 12 | 13 | Args: 14 | lei_responses: List of raw LEI reponse from the API 15 | """ 16 | # Initialize empty lists 17 | self.lei_list = [] 18 | self.lei_names = [] 19 | self.lei_legal_jurisdictions = [] 20 | self.lei_legal_forms = [] 21 | self.lei_other_legal_forms = [] 22 | self.lei_entity_status = [] 23 | self.lei_entity_expiration_dates = [] 24 | self.lei_entity_expiration_reasons = [] 25 | self.lei_successor_entities = [] 26 | self.lei_initial_registration_dates = [] 27 | self.lei_last_update_dates = [] 28 | self.lei_registration_status = [] 29 | self.lei_next_renewal_dates = [] 30 | self.lei_managing_LOU = [] 31 | self.lei_validation_sources = [] 32 | self.lei_associated_lei = [] 33 | self.lei_associated_entity_names = [] 34 | self.lei_associated_entity_types = [] 35 | self.lei_registration_authority_ids = [] 36 | self.lei_other_registration_ids = [] 37 | self.lei_registration_authority_entite_ids = [] 38 | self.lei_entity_categories = [] 39 | self.lei_address_1_line_1 = [] 40 | self.lei_address_1_line_2 = [] 41 | self.lei_address_1_line_3 = [] 42 | self.lei_address_1_line_4 = [] 43 | self.lei_address_1_city = [] 44 | self.lei_address_1_region = [] 45 | self.lei_address_1_country = [] 46 | self.lei_address_1_postal_code = [] 47 | self.lei_address_1_other_type = [] 48 | self.lei_address_1_address_type = [] 49 | self.lei_address_2_line_1 = [] 50 | self.lei_address_2_line_2 = [] 51 | self.lei_address_2_line_3 = [] 52 | self.lei_address_2_line_4 = [] 53 | self.lei_address_2_city = [] 54 | self.lei_address_2_region = [] 55 | self.lei_address_2_country = [] 56 | self.lei_address_2_postal_code = [] 57 | self.lei_address_2_other_type = [] 58 | self.lei_address_2_address_type = [] 59 | self.lei_other_name_name = [] 60 | self.lei_other_name_language = [] 61 | self.lei_other_name_type = [] 62 | self.lei_reporting_exception_1_lei = [] 63 | self.lei_reporting_exception_1_category = [] 64 | self.lei_reporting_exception_2_lei = [] 65 | self.lei_reporting_exception_2_category = [] 66 | self.lei_relationships_1_parent_lei = [] 67 | self.lei_relationships_1_parent_relationship_type = [] 68 | self.lei_relationships_1_parent_relationship_status = [] 69 | self.lei_relationships_1_parent_initial_registration_date = [] 70 | self.lei_relationships_1_parent_last_updated = [] 71 | self.lei_relationships_1_parent_registration_status = [] 72 | self.lei_relationships_1_parent_next_renewal_date = [] 73 | self.lei_relationships_1_parent_managing_LOU = [] 74 | self.lei_relationships_1_parent_validation_sources = [] 75 | self.lei_relationships_1_parent_validation_documents = [] 76 | self.lei_relationships_1_parent_validation_reference = [] 77 | self.lei_relationships_2_parent_lei = [] 78 | self.lei_relationships_2_parent_relationship_type = [] 79 | self.lei_relationships_2_parent_relationship_status = [] 80 | self.lei_relationships_2_parent_initial_registration_date = [] 81 | self.lei_relationships_2_parent_last_updated = [] 82 | self.lei_relationships_2_parent_registration_status = [] 83 | self.lei_relationships_2_parent_next_renewal_date = [] 84 | self.lei_relationships_2_parent_managing_LOU = [] 85 | self.lei_relationships_2_parent_validation_sources = [] 86 | self.lei_relationships_2_parent_validation_documents = [] 87 | self.lei_relationships_2_parent_validation_reference = [] 88 | 89 | if not lei_responses: 90 | raise IndexError("No LEI found") 91 | 92 | for lei_response in lei_responses: 93 | if not lei_response.get("records"): 94 | raise RecordNotFound("No record found") 95 | continue 96 | self.lei_list.append(lei_response["records"][0].get("LEI")) 97 | self.lei_names.append(lei_response["records"][0].get("LegalName")) 98 | self.lei_legal_jurisdictions.append( 99 | lei_response["records"][0].get("LegalJurisdiction") 100 | ) 101 | self.lei_legal_forms.append(lei_response["records"][0].get("LegalForm")) 102 | self.lei_other_legal_forms.append( 103 | lei_response["records"][0].get("OtherLegalForm") 104 | ) 105 | self.lei_entity_status.append( 106 | lei_response["records"][0].get("EntityStatus") 107 | ) 108 | self.lei_entity_expiration_dates.append( 109 | lei_response["records"][0].get("EntityExpirationDate") 110 | ) 111 | self.lei_entity_expiration_reasons.append( 112 | lei_response["records"][0].get("EntityExpirationReason") 113 | ) 114 | self.lei_successor_entities.append( 115 | lei_response["records"][0].get("SuccessorEntity") 116 | ) 117 | self.lei_initial_registration_dates.append( 118 | lei_response["records"][0].get("InitialRegistrationDate") 119 | ) 120 | self.lei_last_update_dates.append( 121 | lei_response["records"][0].get("LastUpdateDate") 122 | ) 123 | self.lei_registration_status.append( 124 | lei_response["records"][0].get("RegistrationStatus") 125 | ) 126 | self.lei_next_renewal_dates.append( 127 | lei_response["records"][0].get("NextRenewalDate") 128 | ) 129 | self.lei_managing_LOU.append(lei_response["records"][0].get("ManagingLOU")) 130 | self.lei_validation_sources.append( 131 | lei_response["records"][0].get("ValidationSources") 132 | ) 133 | self.lei_associated_lei.append( 134 | lei_response["records"][0].get("AssociatedLEI") 135 | ) 136 | self.lei_associated_entity_names.append( 137 | lei_response["records"][0].get("AssociatedEntityName") 138 | ) 139 | self.lei_associated_entity_types.append( 140 | lei_response["records"][0].get("AssociatedEntityType") 141 | ) 142 | self.lei_registration_authority_ids.append( 143 | lei_response["records"][0].get("RegistrationAuthorityID") 144 | ) 145 | self.lei_other_registration_ids.append( 146 | lei_response["records"][0].get("OtherRegistrationAuthorityID") 147 | ) 148 | self.lei_registration_authority_entite_ids.append( 149 | lei_response["records"][0].get("RegistrationAuthorityEntityID") 150 | ) 151 | self.lei_entity_categories.append( 152 | lei_response["records"][0].get("EntityCategory") 153 | ) 154 | self.lei_address_1_line_1.append( 155 | lei_response["records"][0].get("Addresses")[0].get("Line1") 156 | ) 157 | self.lei_address_1_line_2.append( 158 | lei_response["records"][0].get("Addresses")[0].get("Line2") 159 | ) 160 | self.lei_address_1_line_3.append( 161 | lei_response["records"][0].get("Addresses")[0].get("Line3") 162 | ) 163 | self.lei_address_1_line_4.append( 164 | lei_response["records"][0].get("Addresses")[0].get("Line4") 165 | ) 166 | self.lei_address_1_city.append( 167 | lei_response["records"][0].get("Addresses")[0].get("City") 168 | ) 169 | self.lei_address_1_region.append( 170 | lei_response["records"][0].get("Addresses")[0].get("Region") 171 | ) 172 | self.lei_address_1_country.append( 173 | lei_response["records"][0].get("Addresses")[0].get("Country") 174 | ) 175 | self.lei_address_1_postal_code.append( 176 | lei_response["records"][0].get("Addresses")[0].get("PostalCode") 177 | ) 178 | self.lei_address_1_other_type.append( 179 | lei_response["records"][0].get("Addresses")[0].get("OtherType") 180 | ) 181 | self.lei_address_1_address_type.append( 182 | lei_response["records"][0].get("Addresses")[0].get("AddressType") 183 | ) 184 | self.lei_address_2_line_1.append( 185 | lei_response["records"][0].get("Addresses")[1].get("Line1") 186 | ) 187 | self.lei_address_2_line_2.append( 188 | lei_response["records"][0].get("Addresses")[1].get("Line2") 189 | ) 190 | self.lei_address_2_line_3.append( 191 | lei_response["records"][0].get("Addresses")[1].get("Line3") 192 | ) 193 | self.lei_address_2_line_4.append( 194 | lei_response["records"][0].get("Addresses")[1].get("Line4") 195 | ) 196 | self.lei_address_2_city.append( 197 | lei_response["records"][0].get("Addresses")[1].get("City") 198 | ) 199 | self.lei_address_2_region.append( 200 | lei_response["records"][0].get("Addresses")[1].get("Region") 201 | ) 202 | self.lei_address_2_country.append( 203 | lei_response["records"][0].get("Addresses")[1].get("Country") 204 | ) 205 | self.lei_address_2_postal_code.append( 206 | lei_response["records"][0].get("Addresses")[1].get("PostalCode") 207 | ) 208 | self.lei_address_2_other_type.append( 209 | lei_response["records"][0].get("Addresses")[1].get("OtherType") 210 | ) 211 | self.lei_address_2_address_type.append( 212 | lei_response["records"][0].get("Addresses")[1].get("AddressType") 213 | ) 214 | if lei_response["records"][0].get("OtherNames"): 215 | self.lei_other_name_name.append( 216 | lei_response["records"][0].get("OtherNames")[0].get("Name") 217 | ) 218 | self.lei_other_name_language.append( 219 | lei_response["records"][0].get("OtherNames")[0].get("LanguageType") 220 | ) 221 | self.lei_other_name_type.append( 222 | lei_response["records"][0].get("OtherNames")[0].get("NameType") 223 | ) 224 | else: 225 | self.lei_other_name_name.append(None) 226 | self.lei_other_name_language.append(None) 227 | self.lei_other_name_type.append(None) 228 | 229 | if lei_response["records"][0].get("ReportingExceptions"): 230 | self.lei_reporting_exception_1_lei.append( 231 | lei_response["records"][0].get("ReportingExceptions")[0].get("LEI") 232 | ) 233 | self.lei_reporting_exception_1_category.append( 234 | lei_response["records"][0] 235 | .get("ReportingExceptions")[0] 236 | .get("ExceptionCategory") 237 | ) 238 | self.lei_reporting_exception_2_lei.append( 239 | lei_response["records"][0].get("ReportingExceptions")[1].get("LEI") 240 | ) 241 | self.lei_reporting_exception_2_category.append( 242 | lei_response["records"][0] 243 | .get("ReportingExceptions")[1] 244 | .get("ExceptionCategory") 245 | ) 246 | else: 247 | self.lei_reporting_exception_1_lei.append(None) 248 | self.lei_reporting_exception_1_category.append(None) 249 | self.lei_reporting_exception_2_lei.append(None) 250 | self.lei_reporting_exception_2_category.append(None) 251 | if lei_response["records"][0].get("Relationships"): 252 | self.lei_relationships_1_parent_lei.append( 253 | lei_response["records"][0].get("Relationships")[0].get("ParentLEI") 254 | ) 255 | self.lei_relationships_1_parent_relationship_type.append( 256 | lei_response["records"][0] 257 | .get("Relationships")[0] 258 | .get("RelationshipType") 259 | ) 260 | self.lei_relationships_1_parent_relationship_status.append( 261 | lei_response["records"][0] 262 | .get("Relationships")[0] 263 | .get("RelationshipStatus") 264 | ) 265 | self.lei_relationships_1_parent_initial_registration_date.append( 266 | lei_response["records"][0] 267 | .get("Relationships")[0] 268 | .get("InitialRegistrationDate") 269 | ) 270 | self.lei_relationships_1_parent_last_updated.append( 271 | lei_response["records"][0] 272 | .get("Relationships")[0] 273 | .get("LastUpdateDate") 274 | ) 275 | self.lei_relationships_1_parent_registration_status.append( 276 | lei_response["records"][0] 277 | .get("Relationships")[0] 278 | .get("RegistrationStatus") 279 | ) 280 | self.lei_relationships_1_parent_next_renewal_date.append( 281 | lei_response["records"][0] 282 | .get("Relationships")[0] 283 | .get("NextRenewalDate") 284 | ) 285 | self.lei_relationships_1_parent_managing_LOU.append( 286 | lei_response["records"][0] 287 | .get("Relationships")[0] 288 | .get("ManagingLou") 289 | ) 290 | self.lei_relationships_1_parent_validation_sources.append( 291 | lei_response["records"][0] 292 | .get("Relationships")[0] 293 | .get("ValidationSources") 294 | ) 295 | self.lei_relationships_1_parent_validation_documents.append( 296 | lei_response["records"][0] 297 | .get("Relationships")[0] 298 | .get("ValidationDocuments") 299 | ) 300 | self.lei_relationships_1_parent_validation_reference.append( 301 | lei_response["records"][0] 302 | .get("Relationships")[0] 303 | .get("ValidationReference") 304 | ) 305 | self.lei_relationships_2_parent_lei.append( 306 | lei_response["records"][0].get("Relationships")[0].get("ParentLEI") 307 | ) 308 | self.lei_relationships_2_parent_relationship_type.append( 309 | lei_response["records"][0] 310 | .get("Relationships")[0] 311 | .get("RelationshipType") 312 | ) 313 | self.lei_relationships_2_parent_relationship_status.append( 314 | lei_response["records"][0] 315 | .get("Relationships")[0] 316 | .get("RelationshipStatus") 317 | ) 318 | self.lei_relationships_2_parent_initial_registration_date.append( 319 | lei_response["records"][0] 320 | .get("Relationships")[0] 321 | .get("InitialRegistrationDate") 322 | ) 323 | self.lei_relationships_2_parent_last_updated.append( 324 | lei_response["records"][0] 325 | .get("Relationships")[0] 326 | .get("LastUpdateDate") 327 | ) 328 | self.lei_relationships_2_parent_registration_status.append( 329 | lei_response["records"][0] 330 | .get("Relationships")[0] 331 | .get("RegistrationStatus") 332 | ) 333 | self.lei_relationships_2_parent_next_renewal_date.append( 334 | lei_response["records"][0] 335 | .get("Relationships")[0] 336 | .get("NextRenewalDate") 337 | ) 338 | self.lei_relationships_2_parent_managing_LOU.append( 339 | lei_response["records"][0] 340 | .get("Relationships")[0] 341 | .get("ManagingLou") 342 | ) 343 | self.lei_relationships_2_parent_validation_sources.append( 344 | lei_response["records"][0] 345 | .get("Relationships")[0] 346 | .get("ValidationSources") 347 | ) 348 | self.lei_relationships_2_parent_validation_documents.append( 349 | lei_response["records"][0] 350 | .get("Relationships")[0] 351 | .get("ValidationDocuments") 352 | ) 353 | self.lei_relationships_2_parent_validation_reference.append( 354 | lei_response["records"][0] 355 | .get("Relationships")[0] 356 | .get("ValidationReference") 357 | ) 358 | else: 359 | self.lei_relationships_1_parent_lei.append(None) 360 | self.lei_relationships_1_parent_relationship_type.append(None) 361 | self.lei_relationships_1_parent_relationship_status.append(None) 362 | self.lei_relationships_1_parent_initial_registration_date.append(None) 363 | self.lei_relationships_1_parent_last_updated.append(None) 364 | self.lei_relationships_1_parent_registration_status.append(None) 365 | self.lei_relationships_1_parent_next_renewal_date.append(None) 366 | self.lei_relationships_1_parent_managing_LOU.append(None) 367 | self.lei_relationships_1_parent_validation_sources.append(None) 368 | self.lei_relationships_1_parent_validation_documents.append(None) 369 | self.lei_relationships_1_parent_validation_reference.append(None) 370 | self.lei_relationships_2_parent_lei.append(None) 371 | self.lei_relationships_2_parent_relationship_type.append(None) 372 | self.lei_relationships_2_parent_relationship_status.append(None) 373 | self.lei_relationships_2_parent_initial_registration_date.append(None) 374 | self.lei_relationships_2_parent_last_updated.append(None) 375 | self.lei_relationships_2_parent_registration_status.append(None) 376 | self.lei_relationships_2_parent_next_renewal_date.append(None) 377 | self.lei_relationships_2_parent_managing_LOU.append(None) 378 | self.lei_relationships_2_parent_validation_sources.append(None) 379 | self.lei_relationships_2_parent_validation_documents.append(None) 380 | self.lei_relationships_2_parent_validation_reference.append(None) 381 | -------------------------------------------------------------------------------- /src/python_lei/pylei.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pandas as pd 4 | import requests 5 | from dateutil.parser import parse 6 | from python_lei.exceptions import InvalidLEI 7 | from python_lei.parser import LEIParser 8 | 9 | logger = logging.getLogger(__name__) 10 | logger.setLevel(logging.INFO) 11 | 12 | 13 | class pyLEI: 14 | """ 15 | Main class to handle lei requests and return data 16 | """ 17 | 18 | def __init__(self): 19 | """ 20 | Initialize API URL 21 | """ 22 | self.api_url = "https://api.leilex.com/API/LEI/" 23 | 24 | def get_lei_info(self, lei_list, return_dataframe=False): 25 | """ 26 | Request API and return response 27 | 28 | Args: 29 | lei_list: List of LEIs 30 | return_dataframe: Whether to return a dataframe or not. 31 | 32 | Returns: 33 | raw_output: List of Dicts with raw API json response 34 | lei_results: LEI result as class 35 | dataframe: Pandas dataframe with results. 36 | """ 37 | if not isinstance(lei_list, list): 38 | raise AttributeError("Invalid input, please provide LEI in a list") 39 | 40 | if any(lei for lei in lei_list if len(lei) != 20): 41 | raise InvalidLEI("Invalid LEI number found in the list") 42 | 43 | if len(lei_list) > 20: 44 | raise ValueError( 45 | "To respect the free API request quota we have limited the current LEI numbers to 20" 46 | ) 47 | 48 | raw_output = self._request_api(lei_list) 49 | 50 | if not raw_output: 51 | if return_dataframe: 52 | return None, None, None 53 | else: 54 | return None, None 55 | 56 | lei_results = LEIParser(raw_output) 57 | if return_dataframe: 58 | dataframe = pd.DataFrame( 59 | { 60 | "LEI": lei_results.lei_list, 61 | "Legal_Name": lei_results.lei_names, 62 | "Legal_Jurisdiction": lei_results.lei_legal_jurisdictions, 63 | "LegalForm": lei_results.lei_legal_forms, 64 | "Other_Legal_Form": lei_results.lei_other_legal_forms, 65 | "Entity_Status": lei_results.lei_entity_status, 66 | "Entity_Expiration_Date": lei_results.lei_entity_expiration_dates, 67 | "Entity_Expiration_Reason": lei_results.lei_entity_expiration_reasons, 68 | "Successor_Entity": lei_results.lei_successor_entities, 69 | "InitialRegistrationDate": [ 70 | parse(date) 71 | if date else None for date in lei_results.lei_initial_registration_dates 72 | ], 73 | "Last_Update_Date": [ 74 | parse(date) if date else None for date in lei_results.lei_last_update_dates 75 | ], 76 | "Registration_Status": lei_results.lei_registration_status, 77 | "Next_Renewal_Date": lei_results.lei_next_renewal_dates, 78 | "Managing_LOU": lei_results.lei_managing_LOU, 79 | "Validation_Sources": lei_results.lei_validation_sources, 80 | "Associated_LEI": lei_results.lei_associated_lei, 81 | "Associated_Entity_Name": lei_results.lei_associated_entity_names, 82 | "Associated_Entity_Type": lei_results.lei_associated_entity_types, 83 | "Registration_Authority_ID": lei_results.lei_registration_authority_ids, 84 | "Registration_Authority_Entity_ID": lei_results.lei_registration_authority_entite_ids, 85 | "Entity_Category": lei_results.lei_entity_categories, 86 | "Address_1_Line_1": lei_results.lei_address_1_line_1, 87 | "Address_1_Line_2": lei_results.lei_address_1_line_2, 88 | "Address_1_Line_3": lei_results.lei_address_1_line_3, 89 | "Address_1_Line_4": lei_results.lei_address_1_line_4, 90 | "Address_1_city": lei_results.lei_address_1_city, 91 | "Address_1_region": lei_results.lei_address_1_region, 92 | "Address_1_country": lei_results.lei_address_1_country, 93 | "Address_1_postal_code": lei_results.lei_address_1_postal_code, 94 | "Address_1_type": lei_results.lei_address_2_address_type, 95 | "Address_2_Line_1": lei_results.lei_address_2_line_1, 96 | "Address_2_Line_2": lei_results.lei_address_2_line_2, 97 | "Address_2_Line_3": lei_results.lei_address_2_line_3, 98 | "Address_2_Line_4": lei_results.lei_address_2_line_4, 99 | "Address_2_city": lei_results.lei_address_2_city, 100 | "Address_2_region": lei_results.lei_address_2_region, 101 | "Address_2_country": lei_results.lei_address_2_country, 102 | "Address_2_postal_code": lei_results.lei_address_2_postal_code, 103 | "Address_2_type": lei_results.lei_address_2_address_type, 104 | "Other_Name": lei_results.lei_other_name_name, 105 | "Other_Name_Language": lei_results.lei_other_name_language, 106 | "Other_Name_Type": lei_results.lei_other_name_type, 107 | "Relationship_1_Patrent_LEI": lei_results.lei_relationships_1_parent_lei, 108 | "Relationship_1_Relationship_Type": lei_results.lei_relationships_1_parent_relationship_type, 109 | "Relationship_1_Relationship_Status": lei_results.lei_relationships_1_parent_relationship_status, 110 | "Relationship_1_Initial_Registration_Date": [ 111 | parse(date) 112 | if date else None for date in lei_results.lei_relationships_1_parent_initial_registration_date 113 | ], 114 | "Relationship_1_Last_Update_Date": [ 115 | parse(date) 116 | if date else None for date in lei_results.lei_relationships_1_parent_last_updated 117 | ], 118 | "Relationship_1_Registration_Status": lei_results.lei_relationships_1_parent_registration_status, 119 | "Relationship_1_Next_Renewal_Date": [ 120 | parse(date) 121 | if date else None for date in lei_results.lei_relationships_1_parent_next_renewal_date 122 | ], 123 | "Relationship_1_Managing_Lou": lei_results.lei_relationships_1_parent_managing_LOU, 124 | "Relationship_1_Validation_Sources": lei_results.lei_relationships_1_parent_validation_sources, 125 | "Relationship_1_Validation_Documents": lei_results.lei_relationships_1_parent_validation_documents, 126 | "Relationship_1_Validation_Reference": lei_results.lei_relationships_1_parent_validation_reference, 127 | "Relationship_2_Patrent_LEI": lei_results.lei_relationships_2_parent_lei, 128 | "Relationship_2_Relationship_Type": lei_results.lei_relationships_2_parent_relationship_type, 129 | "Relationship_2_Relationship_Status": lei_results.lei_relationships_2_parent_relationship_status, 130 | "Relationship_2_Initial_Registration_Date": [ 131 | parse(date) 132 | if date else None for date in lei_results.lei_relationships_2_parent_initial_registration_date 133 | ], 134 | "Relationship_2_Last_Update_Date": [ 135 | parse(date) 136 | if date else None for date in lei_results.lei_relationships_2_parent_last_updated 137 | ], 138 | "Relationship_2_Registration_Status": lei_results.lei_relationships_2_parent_registration_status, 139 | "Relationship_2_Next_Renewal_Date": [ 140 | parse(date) 141 | if date else None for date in lei_results.lei_relationships_2_parent_next_renewal_date 142 | ], 143 | "Relationship_2_Managing_Lou": lei_results.lei_relationships_2_parent_managing_LOU, 144 | "Relationship_2_Validation_Sources": lei_results.lei_relationships_2_parent_validation_sources, 145 | "Relationship_2_Validation_Documents": lei_results.lei_relationships_2_parent_validation_documents, 146 | "Relationship_2_Validation_Reference": lei_results.lei_relationships_2_parent_validation_reference, 147 | "Reporting_Exceptions_1_LEI": lei_results.lei_reporting_exception_1_lei, 148 | "Reporting_Exceptions_1_Category": lei_results.lei_reporting_exception_1_category, 149 | "Reporting_Exceptions_2_LEI": lei_results.lei_reporting_exception_2_lei, 150 | "Reporting_Exceptions_2_Category": lei_results.lei_reporting_exception_2_category, 151 | } 152 | ) 153 | 154 | return raw_output, lei_results, dataframe 155 | 156 | return raw_output, lei_results 157 | 158 | def _request_api(self, lei_list): 159 | """ 160 | Requests LEI api and obtain the response 161 | 162 | Args: 163 | lei_list: List of LEIs 164 | 165 | Returns: 166 | api_responses: List of Dicts, json response from api 167 | """ 168 | api_responses = [] 169 | 170 | for lei in lei_list: 171 | try: 172 | response = requests.get(f"{self.api_url}/{lei}") 173 | except requests.exceptions.ConnectionError as err: 174 | logger.error("Error connecting to API host", exc_info=err) 175 | continue 176 | 177 | if not response.ok: 178 | logger.warning(f"No response from API for LEI number {lei}") 179 | continue 180 | 181 | api_responses.append(response.json()) 182 | 183 | return api_responses 184 | -------------------------------------------------------------------------------- /src/python_lei/utils.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | import os 4 | import shutil 5 | import zipfile 6 | from datetime import date 7 | from pathlib import Path 8 | 9 | import pandas as pd 10 | import requests 11 | from bs4 import BeautifulSoup 12 | 13 | logger = logging.getLogger(__name__) 14 | logging.basicConfig(level=logging.INFO) 15 | 16 | 17 | PROJECT_ROOT = Path(__file__).parent.parent.parent 18 | RESOURCE_DIR = os.path.join(PROJECT_ROOT, "resources") 19 | TODAY = date.today().strftime("%Y%m%d") 20 | 21 | 22 | class Download: 23 | """ 24 | Download LEI to ISIN mappings 25 | """ 26 | 27 | def __init__(self, _is_actions=False): 28 | """ 29 | Downloads LEI ISIN mapping 30 | 31 | Args: 32 | _is_actions (bool): For setting path of downloaded resources on Github Actions 33 | """ 34 | self.data_url = "https://www.gleif.org/en/lei-data/lei-mapping/download-isin-to-lei-relationship-files" 35 | self._download(_is_actions) 36 | 37 | def _download(self, _is_actions): 38 | """ 39 | Initiate download into resource folder 40 | """ 41 | if not os.path.exists(RESOURCE_DIR): 42 | logger.info(f"No resources directory found, creating resources directory.") 43 | os.mkdir(RESOURCE_DIR) 44 | 45 | download_link = self._scrape_isin_file() 46 | 47 | if not download_link: 48 | raise ValueError("Downloading of isin file not available.") 49 | 50 | try: 51 | response = requests.get(download_link) 52 | except requests.exceptions as err: 53 | logger.error( 54 | "Connection Error, Unable to download data at this time. Please check you have working internet connection or try again later." 55 | ) 56 | if not response.ok: 57 | logger.error("No response from GLEIF server.") 58 | 59 | logger.info("The file could be over 50 Mb.") 60 | zipped_content = zipfile.ZipFile(io.BytesIO(response.content)) 61 | if _is_actions: 62 | zipped_content.extractall( 63 | "/home/runner/work/python-lei/python-lei/resources" 64 | ) 65 | else: 66 | zipped_content.extractall(RESOURCE_DIR) 67 | logger.info(f"Extraction complete in {RESOURCE_DIR}") 68 | 69 | def _scrape_isin_file(self): 70 | """ 71 | Scrape the data. 72 | """ 73 | try: 74 | response = requests.get(self.data_url) 75 | response.raise_for_status() 76 | soup = BeautifulSoup(response.text) 77 | 78 | # find all the tr and td and get to the href 79 | download_link = soup.find_all("tr")[1].find_all("td")[1].find("a")["href"] 80 | return download_link 81 | 82 | except requests.ConnectionError: 83 | logger.error(f"Error connecting to {self.data_url}") 84 | 85 | 86 | class Update: 87 | """ 88 | Update the ISIN mapping in the resource folder 89 | """ 90 | 91 | def __init__(self): 92 | """ 93 | Update the ISIN LEI data 94 | """ 95 | # Remove the old data and call the download class 96 | 97 | if ( 98 | not os.path.exists(RESOURCE_DIR) or os.listdir(RESOURCE_DIR) == [] 99 | ): # TODO: use not 100 | logger.info( 101 | "Resource directory not found or LEI ISIN mappings not found. Downloading now." 102 | ) 103 | Download() 104 | 105 | if os.listdir(RESOURCE_DIR) != []: 106 | shutil.rmtree(RESOURCE_DIR) 107 | logger.info(f"Downloading Data in {RESOURCE_DIR}") 108 | Download() 109 | 110 | 111 | def load_data(): 112 | """ 113 | Loads and Returns the dataframe 114 | """ 115 | dataframe = pd.read_csv(os.path.join(RESOURCE_DIR, os.listdir(RESOURCE_DIR)[0])) 116 | return dataframe 117 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdvala/python-lei/ad16ad9b44012780d9c44a1d4462fe601187f86c/tests/__init__.py -------------------------------------------------------------------------------- /tests/assets/dataframe_getleiinfo.csv: -------------------------------------------------------------------------------- 1 | LEI,Legal_Name,Legal_Jurisdiction,LegalForm,Other_Legal_Form,Entity_Status,Entity_Expiration_Date,Entity_Expiration_Reason,Successor_Entity,InitialRegistrationDate,Last_Update_Date,Registration_Status,Next_Renewal_Date,Managing_LOU,Validation_Sources,Associated_LEI,Associated_Entity_Name,Associated_Entity_Type,Registration_Authority_ID,Registration_Authority_Entity_ID,Entity_Category,Address_1_Line_1,Address_1_Line_2,Address_1_Line_3,Address_1_Line_4,Address_1_city,Address_1_region,Address_1_country,Address_1_postal_code,Address_1_type,Address_2_Line_1,Address_2_Line_2,Address_2_Line_3,Address_2_Line_4,Address_2_city,Address_2_region,Address_2_country,Address_2_postal_code,Address_2_type,Other_Name,Other_Name_Language,Other_Name_Type,Reporting_Exceptions_1_LEI,Reporting_Exceptions_1_Category,Reporting_Exceptions_2_LEI,Reporting_Exceptions_2_Category 2 | A23RUXWKASG834LTMK28,"AUTOLIV, INC.",US-DE,XTIQ,,ACTIVE,,,,2012-06-06 03:52:00+00:00,2019-12-18 03:32:00+00:00,ISSUED,2020-12-15T10:15:00.000 +00:00,EVK05KS7XY1DEII3R011,FULLY_CORROBORATED,,,,RA000602 ,2155072,,Box 70381,,,,Stockholm,SE-AB,SE,107 24,LEGAL_ADDRESS,C/O THE CORPORATION TRUST COMPANY,CORPORATION TRUST CENTER 1209 ORANGE ST,,,WILMINGTON,US-DE,US,19801,LEGAL_ADDRESS,,,,A23RUXWKASG834LTMK28,DIRECT_ACCOUNTING_CONSOLIDATION_PARENT,A23RUXWKASG834LTMK28,ULTIMATE_ACCOUNTING_CONSOLIDATION_PARENT 3 | -------------------------------------------------------------------------------- /tests/assets/isin_response.csv: -------------------------------------------------------------------------------- 1 | LEI,ISIN 2 | A23RUXWKASG834LTMK28,SE0000382335 3 | A23RUXWKASG834LTMK28,US052800AB59 4 | A23RUXWKASG834LTMK28,US0528002084 5 | A23RUXWKASG834LTMK28,US0528003074 6 | A23RUXWKASG834LTMK28,US0528001094 7 | A23RUXWKASG834LTMK28,US0528001177 8 | -------------------------------------------------------------------------------- /tests/assets/test_lei_isin.csv: -------------------------------------------------------------------------------- 1 | LEI,ISIN 2 | A23RUXWKASG834LTMK28,SE0000382335 3 | A23RUXWKASG834LTMK28,US052800AB59 4 | A23RUXWKASG834LTMK28,US0528002084 5 | A23RUXWKASG834LTMK28,US0528003074 6 | A23RUXWKASG834LTMK28,US0528001094 7 | A23RUXWKASG834LTMK28,US0528001177 -------------------------------------------------------------------------------- /tests/test_lei_search.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | from python_lei.lei_search import SearchLEI 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def lei_search_response(): 9 | return [ 10 | {"LegalName": "Cleantech Benelux SPRL", "LEI": "549300DON6QM36Z6CQ75"}, 11 | {"LegalName": "CLEANTECH EUROPE I (A) LP", "LEI": "2138006HK4WI7O5AZZ55"}, 12 | {"LegalName": "Cleantech Management GmbH", "LEI": "391200V6W1JRTEX52V10"}, 13 | {"LegalName": "Cleantech Treuvermögen GmbH", "LEI": "391200GQTDEQKN7T4U12"}, 14 | {"LegalName": "Cleantech Infrastruktur GmbH", "LEI": "391200NWGDYHODS1IZ56"}, 15 | { 16 | "LegalName": "CLEANTECH BUILDING MATERIALS PLC", 17 | "LEI": "213800AF6AQVK2PFVY02", 18 | }, 19 | { 20 | "LegalName": "Cleantech Vision PV 10 GmbH & Co KG", 21 | "LEI": "391200DYVTFX12QHSL88", 22 | }, 23 | { 24 | "LegalName": "Cleantech Vision PV 13 GmbH & Co KG", 25 | "LEI": "391200LFEW46QDOULC94", 26 | }, 27 | { 28 | "LegalName": "Cleantech Vision PV 14 GmbH & Co KG", 29 | "LEI": "3912007UV23HZYGP7P98", 30 | }, 31 | { 32 | "LegalName": "CLEANTECH EUROPE II LUXEMBOURG S.À R.L.", 33 | "LEI": "213800T4PUCAV91BS318", 34 | }, 35 | { 36 | "LegalName": "CLEANTECH SOLAR ENERGY (INDIA) PRIVATE LIMITED", 37 | "LEI": "335800DYTSIUIRGDOY08", 38 | }, 39 | { 40 | "LegalName": "Cleantech Infrastrukturgesellschaft mbH & Co. KG", 41 | "LEI": "391200BCW28KLMPO7X74", 42 | }, 43 | { 44 | "LegalName": "AC Cleantech Growth Fund I Holding AB", 45 | "LEI": "549300WEAJ76PQLCY106", 46 | }, 47 | {"LegalName": "KSL CLEANTECH LIMITED", "LEI": "9845006C6DBHCB91TA46"}, 48 | {"LegalName": "SBG CLEANTECH LIMITED", "LEI": "984500BA390DEEC8A198"}, 49 | {"LegalName": "SFN Cleantech Investment Ltd", "LEI": "5299001U27KH2TE1HA16"}, 50 | { 51 | "LegalName": "SBG CLEANTECH PROJECTCO PRIVATE LIMITED", 52 | "LEI": "335800L6KBRBMN23G491", 53 | }, 54 | { 55 | "LegalName": "SBG CLEANTECH ENERGY EIGHT PRIVATE LIMITED", 56 | "LEI": "33580025O3MECVXEXN20", 57 | }, 58 | { 59 | "LegalName": "SBG CLEANTECH PROJECTCO FIVE PRIVATE LIMITED", 60 | "LEI": "335800VKNBX6UT5XTA61", 61 | }, 62 | {"LegalName": "AMPL CLEANTECH PRIVATE LIMITED", "LEI": "3358003BN2TSHLZFK835"}, 63 | {"LegalName": "TATA CLEANTECH CAPITAL LIMITED", "LEI": "335800VHGHY6ACK4EV23"}, 64 | { 65 | "LegalName": "OPEN CLEANTECH INCOME SECURITIES DAC", 66 | "LEI": "635400RZAFJOB3F3HO02", 67 | }, 68 | { 69 | "LegalName": "ACME CLEANTECH SOLUTIONS PRIVATE LIMITED", 70 | "LEI": "335800Y9QOUKCKEBDN64", 71 | }, 72 | {"LegalName": "FFG - CLEANTECH II", "LEI": "549300BOYS2N4DQ86621"}, 73 | {"LegalName": "Ikaros Cleantech AB", "LEI": "529900F3ILO82OOHJ624"}, 74 | {"LegalName": "ARENKO CLEANTECH LIMITED", "LEI": "213800XJAKB99U6GYE81"}, 75 | { 76 | "LegalName": "CHORUS CleanTech PP Life GmbH & Co. 8. KG", 77 | "LEI": "39120001P89AEAGP4C95", 78 | }, 79 | { 80 | "LegalName": "Vierte Cleantech Infrastrukturgesellschaft mbH", 81 | "LEI": "391200MHC20BINNZSM38", 82 | }, 83 | { 84 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Vilseck KG", 85 | "LEI": "39120001CK4NXMUXLH16", 86 | }, 87 | { 88 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Vilseck KG", 89 | "LEI": "39120001CK4NXMUXLH82", 90 | }, 91 | { 92 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Bockelwitz KG", 93 | "LEI": "39120001IO1IEBNJB658", 94 | }, 95 | { 96 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Bockelwitz KG", 97 | "LEI": "39120001IO1IEBNJB640", 98 | }, 99 | { 100 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Richelbach KG", 101 | "LEI": "39120001I4DLSGVIED33", 102 | }, 103 | { 104 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Richelbach KG", 105 | "LEI": "39120001I4DLSGVIED65", 106 | }, 107 | { 108 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarparks Niederbayern KG", 109 | "LEI": "391200XOFXB6SEDEUE64", 110 | }, 111 | { 112 | "LegalName": "Dritte Cleantech Infrastrukturgesellschaft mbH & Co. KG", 113 | "LEI": "391200D4IQYJZH3PQV66", 114 | }, 115 | { 116 | "LegalName": "Fünfte Cleantech Infrastrukturgesellschaft mbH & Co. KG", 117 | "LEI": "391200YSBV1PTJDIVU78", 118 | }, 119 | { 120 | "LegalName": "Zweite Cleantech Infrastrukturgesellschaft mbH & Co. KG", 121 | "LEI": "391200B0QA2GDAGUWL36", 122 | }, 123 | {"LegalName": "Kentara Cleantech S.à r.l.", "LEI": "549300O1LH5FYDMNBI61"}, 124 | { 125 | "LegalName": "TRINITY CLEANTECH PRIVATE LIMITED", 126 | "LEI": "3358007KMDXWXRDRLJ07", 127 | }, 128 | {"LegalName": "FUNDO EDP CLEANTECH FCR", "LEI": "529900YNVY87YJXQQL73"}, 129 | {"LegalName": "CAPRICORN CLEANTECH FUND", "LEI": "5493000VTLH53534F243"}, 130 | { 131 | "LegalName": "SBSR POWER CLEANTECH ELEVEN PRIVATE LIMITED", 132 | "LEI": "335800PDQXWJSCUT1763", 133 | }, 134 | { 135 | "LegalName": "ThomasLloyd Cleantech Infrastructure Fund SICAV", 136 | "LEI": "391200IW2I7VHRGVL291", 137 | }, 138 | { 139 | "LegalName": "ThomasLloyd Cleantech Infrastructure (Czech) a.s.", 140 | "LEI": "391200SNUDPCVPZHNT11", 141 | }, 142 | { 143 | "LegalName": "ThomasLloyd Cleantech Infrastructure Holding GmbH", 144 | "LEI": "391200RVK5MPKRZEMA60", 145 | }, 146 | { 147 | "LegalName": "ThomasLloyd Cleantech Infrastructure Asia Holding GmbH", 148 | "LEI": "39120062GAN8OFYXWW72", 149 | }, 150 | { 151 | "LegalName": "ThomasLloyd Cleantech Infrastructure (Liechtenstein) AG", 152 | "LEI": "391200KABA0XCWJDTK89", 153 | }, 154 | { 155 | "LegalName": "Desjardins SocieTerra Cleantech Fund", 156 | "LEI": "54930032H86FEEE8KT71", 157 | }, 158 | { 159 | "LegalName": "INVESCO EXCHANGE-TRADED FUND TRUST - Invesco Cleantech ETF", 160 | "LEI": "549300O3D3IUFT3CTM23", 161 | }, 162 | ] 163 | 164 | 165 | def test_search_lei(lei_search_response): 166 | possible_lei = SearchLEI() 167 | with patch.object(SearchLEI, "_request_api", return_value=lei_search_response): 168 | raw_output = possible_lei.search_lei("CleanTech") 169 | 170 | assert raw_output == [ 171 | {"LegalName": "Cleantech Benelux SPRL", "LEI": "549300DON6QM36Z6CQ75"}, 172 | {"LegalName": "CLEANTECH EUROPE I (A) LP", "LEI": "2138006HK4WI7O5AZZ55"}, 173 | {"LegalName": "Cleantech Management GmbH", "LEI": "391200V6W1JRTEX52V10"}, 174 | {"LegalName": "Cleantech Treuvermögen GmbH", "LEI": "391200GQTDEQKN7T4U12"}, 175 | {"LegalName": "Cleantech Infrastruktur GmbH", "LEI": "391200NWGDYHODS1IZ56"}, 176 | { 177 | "LegalName": "CLEANTECH BUILDING MATERIALS PLC", 178 | "LEI": "213800AF6AQVK2PFVY02", 179 | }, 180 | { 181 | "LegalName": "Cleantech Vision PV 10 GmbH & Co KG", 182 | "LEI": "391200DYVTFX12QHSL88", 183 | }, 184 | { 185 | "LegalName": "Cleantech Vision PV 13 GmbH & Co KG", 186 | "LEI": "391200LFEW46QDOULC94", 187 | }, 188 | { 189 | "LegalName": "Cleantech Vision PV 14 GmbH & Co KG", 190 | "LEI": "3912007UV23HZYGP7P98", 191 | }, 192 | { 193 | "LegalName": "CLEANTECH EUROPE II LUXEMBOURG S.À R.L.", 194 | "LEI": "213800T4PUCAV91BS318", 195 | }, 196 | { 197 | "LegalName": "CLEANTECH SOLAR ENERGY (INDIA) PRIVATE LIMITED", 198 | "LEI": "335800DYTSIUIRGDOY08", 199 | }, 200 | { 201 | "LegalName": "Cleantech Infrastrukturgesellschaft mbH & Co. KG", 202 | "LEI": "391200BCW28KLMPO7X74", 203 | }, 204 | { 205 | "LegalName": "AC Cleantech Growth Fund I Holding AB", 206 | "LEI": "549300WEAJ76PQLCY106", 207 | }, 208 | {"LegalName": "KSL CLEANTECH LIMITED", "LEI": "9845006C6DBHCB91TA46"}, 209 | {"LegalName": "SBG CLEANTECH LIMITED", "LEI": "984500BA390DEEC8A198"}, 210 | {"LegalName": "SFN Cleantech Investment Ltd", "LEI": "5299001U27KH2TE1HA16"}, 211 | { 212 | "LegalName": "SBG CLEANTECH PROJECTCO PRIVATE LIMITED", 213 | "LEI": "335800L6KBRBMN23G491", 214 | }, 215 | { 216 | "LegalName": "SBG CLEANTECH ENERGY EIGHT PRIVATE LIMITED", 217 | "LEI": "33580025O3MECVXEXN20", 218 | }, 219 | { 220 | "LegalName": "SBG CLEANTECH PROJECTCO FIVE PRIVATE LIMITED", 221 | "LEI": "335800VKNBX6UT5XTA61", 222 | }, 223 | {"LegalName": "AMPL CLEANTECH PRIVATE LIMITED", "LEI": "3358003BN2TSHLZFK835"}, 224 | {"LegalName": "TATA CLEANTECH CAPITAL LIMITED", "LEI": "335800VHGHY6ACK4EV23"}, 225 | { 226 | "LegalName": "OPEN CLEANTECH INCOME SECURITIES DAC", 227 | "LEI": "635400RZAFJOB3F3HO02", 228 | }, 229 | { 230 | "LegalName": "ACME CLEANTECH SOLUTIONS PRIVATE LIMITED", 231 | "LEI": "335800Y9QOUKCKEBDN64", 232 | }, 233 | {"LegalName": "FFG - CLEANTECH II", "LEI": "549300BOYS2N4DQ86621"}, 234 | {"LegalName": "Ikaros Cleantech AB", "LEI": "529900F3ILO82OOHJ624"}, 235 | {"LegalName": "ARENKO CLEANTECH LIMITED", "LEI": "213800XJAKB99U6GYE81"}, 236 | { 237 | "LegalName": "CHORUS CleanTech PP Life GmbH & Co. 8. KG", 238 | "LEI": "39120001P89AEAGP4C95", 239 | }, 240 | { 241 | "LegalName": "Vierte Cleantech Infrastrukturgesellschaft mbH", 242 | "LEI": "391200MHC20BINNZSM38", 243 | }, 244 | { 245 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Vilseck KG", 246 | "LEI": "39120001CK4NXMUXLH16", 247 | }, 248 | { 249 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Vilseck KG", 250 | "LEI": "39120001CK4NXMUXLH82", 251 | }, 252 | { 253 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Bockelwitz KG", 254 | "LEI": "39120001IO1IEBNJB658", 255 | }, 256 | { 257 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Bockelwitz KG", 258 | "LEI": "39120001IO1IEBNJB640", 259 | }, 260 | { 261 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Richelbach KG", 262 | "LEI": "39120001I4DLSGVIED33", 263 | }, 264 | { 265 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarpark Richelbach KG", 266 | "LEI": "39120001I4DLSGVIED65", 267 | }, 268 | { 269 | "LegalName": "CHORUS CleanTech GmbH & Co. Solarparks Niederbayern KG", 270 | "LEI": "391200XOFXB6SEDEUE64", 271 | }, 272 | { 273 | "LegalName": "Dritte Cleantech Infrastrukturgesellschaft mbH & Co. KG", 274 | "LEI": "391200D4IQYJZH3PQV66", 275 | }, 276 | { 277 | "LegalName": "Fünfte Cleantech Infrastrukturgesellschaft mbH & Co. KG", 278 | "LEI": "391200YSBV1PTJDIVU78", 279 | }, 280 | { 281 | "LegalName": "Zweite Cleantech Infrastrukturgesellschaft mbH & Co. KG", 282 | "LEI": "391200B0QA2GDAGUWL36", 283 | }, 284 | {"LegalName": "Kentara Cleantech S.à r.l.", "LEI": "549300O1LH5FYDMNBI61"}, 285 | { 286 | "LegalName": "TRINITY CLEANTECH PRIVATE LIMITED", 287 | "LEI": "3358007KMDXWXRDRLJ07", 288 | }, 289 | {"LegalName": "FUNDO EDP CLEANTECH FCR", "LEI": "529900YNVY87YJXQQL73"}, 290 | {"LegalName": "CAPRICORN CLEANTECH FUND", "LEI": "5493000VTLH53534F243"}, 291 | { 292 | "LegalName": "SBSR POWER CLEANTECH ELEVEN PRIVATE LIMITED", 293 | "LEI": "335800PDQXWJSCUT1763", 294 | }, 295 | { 296 | "LegalName": "ThomasLloyd Cleantech Infrastructure Fund SICAV", 297 | "LEI": "391200IW2I7VHRGVL291", 298 | }, 299 | { 300 | "LegalName": "ThomasLloyd Cleantech Infrastructure (Czech) a.s.", 301 | "LEI": "391200SNUDPCVPZHNT11", 302 | }, 303 | { 304 | "LegalName": "ThomasLloyd Cleantech Infrastructure Holding GmbH", 305 | "LEI": "391200RVK5MPKRZEMA60", 306 | }, 307 | { 308 | "LegalName": "ThomasLloyd Cleantech Infrastructure Asia Holding GmbH", 309 | "LEI": "39120062GAN8OFYXWW72", 310 | }, 311 | { 312 | "LegalName": "ThomasLloyd Cleantech Infrastructure (Liechtenstein) AG", 313 | "LEI": "391200KABA0XCWJDTK89", 314 | }, 315 | { 316 | "LegalName": "Desjardins SocieTerra Cleantech Fund", 317 | "LEI": "54930032H86FEEE8KT71", 318 | }, 319 | { 320 | "LegalName": "INVESCO EXCHANGE-TRADED FUND TRUST - Invesco Cleantech ETF", 321 | "LEI": "549300O3D3IUFT3CTM23", 322 | }, 323 | ] 324 | -------------------------------------------------------------------------------- /tests/test_pylei.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pandas as pd 4 | import pytest 5 | from python_lei.exceptions import InvalidLEI 6 | from python_lei.pylei import pyLEI 7 | from python_lei.utils import PROJECT_ROOT 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def lei_api_response(): 12 | return [ 13 | { 14 | "total_record_count": 1, 15 | "page_number": 1, 16 | "page_size": 100, 17 | "total_pages": 1, 18 | "has_more": False, 19 | "records": [ 20 | { 21 | "LEI": "A23RUXWKASG834LTMK28", 22 | "LegalName": "AUTOLIV, INC.", 23 | "LegalJurisdiction": "US-DE", 24 | "LegalForm": "XTIQ", 25 | "OtherLegalForm": "", 26 | "EntityStatus": "ACTIVE", 27 | "EntityExpirationDate": None, 28 | "EntityExpirationReason": "", 29 | "SuccessorEntity": "", 30 | "InitialRegistrationDate": "2012-06-06T03:52:00.000 +00:00", 31 | "LastUpdateDate": "2019-12-18T03:32:00.000 +00:00", 32 | "RegistrationStatus": "ISSUED", 33 | "NextRenewalDate": "2020-12-15T10:15:00.000 +00:00", 34 | "ManagingLOU": "EVK05KS7XY1DEII3R011", 35 | "ValidationSources": "FULLY_CORROBORATED", 36 | "AssociatedLEI": "", 37 | "AssociatedEntityName": "", 38 | "AssociatedEntityType": "", 39 | "RegistrationAuthorityID": "RA000602 ", 40 | "OtherRegistrationAuthorityID": "", 41 | "RegistrationAuthorityEntityID": "2155072", 42 | "EntityCategory": "", 43 | "Addresses": [ 44 | { 45 | "Line1": "Box 70381", 46 | "Line2": "", 47 | "Line3": "", 48 | "Line4": "", 49 | "City": "Stockholm", 50 | "Region": "SE-AB", 51 | "Country": "SE", 52 | "PostalCode": "107 24", 53 | "OtherType": "", 54 | "AddressType": "HEADQUARTERS_ADDRESS", 55 | }, 56 | { 57 | "Line1": "C/O THE CORPORATION TRUST COMPANY", 58 | "Line2": "CORPORATION TRUST CENTER 1209 ORANGE ST", 59 | "Line3": "", 60 | "Line4": "", 61 | "City": "WILMINGTON", 62 | "Region": "US-DE", 63 | "Country": "US", 64 | "PostalCode": "19801", 65 | "OtherType": "", 66 | "AddressType": "LEGAL_ADDRESS", 67 | }, 68 | ], 69 | "OtherNames": [], 70 | "ValidationAuthorities": [ 71 | { 72 | "ValidationAuthorityID": "RA000602", 73 | "OtherValidationAuthorityID": "", 74 | "ValidationAuthorityEntityID": "2155072", 75 | } 76 | ], 77 | "Relationships": [], 78 | "ReportingExceptions": [ 79 | { 80 | "LEI": "A23RUXWKASG834LTMK28", 81 | "ExceptionCategory": "DIRECT_ACCOUNTING_CONSOLIDATION_PARENT", 82 | "ExceptionReasons": [{"Reason": "NON_CONSOLIDATING"}], 83 | "ExceptionReferences": [], 84 | }, 85 | { 86 | "LEI": "A23RUXWKASG834LTMK28", 87 | "ExceptionCategory": "ULTIMATE_ACCOUNTING_CONSOLIDATION_PARENT", 88 | "ExceptionReasons": [{"Reason": "NON_CONSOLIDATING"}], 89 | "ExceptionReferences": [], 90 | }, 91 | ], 92 | } 93 | ], 94 | } 95 | ] 96 | 97 | 98 | def test_get_lei_info(lei_api_response): 99 | getleiinfo = pyLEI() 100 | with patch.object(pyLEI, "_request_api", return_value=lei_api_response): 101 | raw_output, lei_results = getleiinfo.get_lei_info(["A23RUXWKASG834LTMK28"]) 102 | 103 | assert raw_output == [ 104 | { 105 | "total_record_count": 1, 106 | "page_number": 1, 107 | "page_size": 100, 108 | "total_pages": 1, 109 | "has_more": False, 110 | "records": [ 111 | { 112 | "LEI": "A23RUXWKASG834LTMK28", 113 | "LegalName": "AUTOLIV, INC.", 114 | "LegalJurisdiction": "US-DE", 115 | "LegalForm": "XTIQ", 116 | "OtherLegalForm": "", 117 | "EntityStatus": "ACTIVE", 118 | "EntityExpirationDate": None, 119 | "EntityExpirationReason": "", 120 | "SuccessorEntity": "", 121 | "InitialRegistrationDate": "2012-06-06T03:52:00.000 +00:00", 122 | "LastUpdateDate": "2019-12-18T03:32:00.000 +00:00", 123 | "RegistrationStatus": "ISSUED", 124 | "NextRenewalDate": "2020-12-15T10:15:00.000 +00:00", 125 | "ManagingLOU": "EVK05KS7XY1DEII3R011", 126 | "ValidationSources": "FULLY_CORROBORATED", 127 | "AssociatedLEI": "", 128 | "AssociatedEntityName": "", 129 | "AssociatedEntityType": "", 130 | "RegistrationAuthorityID": "RA000602 ", 131 | "OtherRegistrationAuthorityID": "", 132 | "RegistrationAuthorityEntityID": "2155072", 133 | "EntityCategory": "", 134 | "Addresses": [ 135 | { 136 | "Line1": "Box 70381", 137 | "Line2": "", 138 | "Line3": "", 139 | "Line4": "", 140 | "City": "Stockholm", 141 | "Region": "SE-AB", 142 | "Country": "SE", 143 | "PostalCode": "107 24", 144 | "OtherType": "", 145 | "AddressType": "HEADQUARTERS_ADDRESS", 146 | }, 147 | { 148 | "Line1": "C/O THE CORPORATION TRUST COMPANY", 149 | "Line2": "CORPORATION TRUST CENTER 1209 ORANGE ST", 150 | "Line3": "", 151 | "Line4": "", 152 | "City": "WILMINGTON", 153 | "Region": "US-DE", 154 | "Country": "US", 155 | "PostalCode": "19801", 156 | "OtherType": "", 157 | "AddressType": "LEGAL_ADDRESS", 158 | }, 159 | ], 160 | "OtherNames": [], 161 | "ValidationAuthorities": [ 162 | { 163 | "ValidationAuthorityID": "RA000602", 164 | "OtherValidationAuthorityID": "", 165 | "ValidationAuthorityEntityID": "2155072", 166 | } 167 | ], 168 | "Relationships": [], 169 | "ReportingExceptions": [ 170 | { 171 | "LEI": "A23RUXWKASG834LTMK28", 172 | "ExceptionCategory": "DIRECT_ACCOUNTING_CONSOLIDATION_PARENT", 173 | "ExceptionReasons": [{"Reason": "NON_CONSOLIDATING"}], 174 | "ExceptionReferences": [], 175 | }, 176 | { 177 | "LEI": "A23RUXWKASG834LTMK28", 178 | "ExceptionCategory": "ULTIMATE_ACCOUNTING_CONSOLIDATION_PARENT", 179 | "ExceptionReasons": [{"Reason": "NON_CONSOLIDATING"}], 180 | "ExceptionReferences": [], 181 | }, 182 | ], 183 | } 184 | ], 185 | } 186 | ] 187 | 188 | assert lei_results.lei_names == ["AUTOLIV, INC."] 189 | assert lei_results.lei_list == ["A23RUXWKASG834LTMK28"] 190 | 191 | with patch.object(pyLEI, "_request_api", return_value=lei_api_response): 192 | raw_output, lei_results, dataframe = getleiinfo.get_lei_info( 193 | ["A23RUXWKASG834LTMK28"], return_dataframe=True 194 | ) 195 | 196 | df_lei_info = pd.read_csv(f"{PROJECT_ROOT}/tests/assets/dataframe_getleiinfo.csv") 197 | # TODO: Check with pandas 198 | # pd.testing.assert_frame_equal(df_lei_info, dataframe, check_dtype = False) 199 | 200 | assert df_lei_info["LEI"].tolist() == dataframe["LEI"].tolist() 201 | assert df_lei_info["Legal_Name"].tolist() == dataframe["Legal_Name"].tolist() 202 | 203 | 204 | def test_get_lei_info_not_list(): 205 | getleiinfo = pyLEI() 206 | with pytest.raises(AttributeError) as exc_info: 207 | getleiinfo.get_lei_info("A23RUXWKASG834LTMK2") 208 | assert str(exc_info.value) == "Invalid input, please provide LEI in a list" 209 | 210 | 211 | def test_get_lei_info_invalid_lei(): 212 | getleiinfo = pyLEI() 213 | with pytest.raises(InvalidLEI) as exc_info: 214 | getleiinfo.get_lei_info(["A23RUXWKASG834LTK2"]) 215 | assert str(exc_info.value) == "Invalid LEI number found in the list" 216 | 217 | 218 | def test_get_lei_info_lei_list_long(): 219 | getleiinfo = pyLEI() 220 | with pytest.raises(ValueError) as exc_info: 221 | getleiinfo.get_lei_info(["A23RUXWKASG834LTMK28"] * 25) 222 | assert ( 223 | str(exc_info.value) 224 | == "To respect the free API request quota we have limited the current LEI numbers to 20" 225 | ) 226 | --------------------------------------------------------------------------------