diff options
author | dio <kulbinderdio@gmail.com> | 2023-08-28 15:28:33 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-28 14:28:33 +0000 |
commit | db9d462188d6115d9a3e215ea3b633bb26fcd97e (patch) | |
tree | 9195c68605dc2b517124abd2212a3073170cf29f | |
parent | f8241a6b81f343f358b01927c0abd9ac693a9be6 (diff) |
Feature/companies house (#4721)
* Addition of UK Companies House data. Allows you to search for companies and inspect their details and download any filings including accounts
* reformatting of code
* reformatting
* reformat code
* reformat
* fix codespell issue
* updated for changes in way key are handled
* small reformatting
* added timeout for requests
* formatting
* ruff order fixes
* black reformatting
* merge issue - removed function call_openbb
* black format change
* Added new Companies House functionality to SDK
* formattng
* more Formatting (black)
* Ruff fix
* change return type for function
* forced type conversion
* return type fix
* correct error return type
* fix return type
* deleted companieshouse tests as they require API key
* Extra validation added for when data not exist from remote calls
* Added extra commands to en.yml
* spelling mistake correction
* fat finger trouble corrected
* Save documents with identifying names and allow documents to be viewed directly within OpenBB
* ruff updates
* black changes
* ruff change
* Addition of currently loaded company information in menu
* add entry in the API keys guide for Companies Hosue
* adds images to api keys guide
* adds section in SDK API Keys Guide for Companies House
* test file for companieshouse_model
* changed test data due to ruff line size limit
* resolve merge issues
* resolve merge issue
* doc strings examples
* added docstrings for companieshouse_model
* allow download_filking_document methid in view class to be called from SDK
* added docstring for download_filing_document SDK method
* correct Companies house key literal
* replace starnge . with ,
* add check_api_key decorator to each methopd in controller
* implement next and previous for looping through filings
* ruff changes
* correct tests
* companing filings now allows you specify category
* ruff line length issue resolved
* retrieves all filings and added charges command
* bug fix
* Added limit parameter to filings command, def :100
* fix ruff issue
* added get_charges to sdk and to test class
* resolved ruff line length issue
* add some view tests
* add some controller tests
* applying fix not merged to develop yet, column_keep_types=['Company Number']
* fix i18n description
* update test_print_help
* actually update test_print_help.txt
* Update en.yml - no colon
* rerorder imports - ruff failurre
* simple text change
* ruff fix
* help text change trying to fix pytest issue
---------
Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>
30 files changed, 2174 insertions, 11 deletions
diff --git a/openbb_terminal/alternative/alt_controller.py b/openbb_terminal/alternative/alt_controller.py index cc4e7e1713d..4352e8193df 100644 --- a/openbb_terminal/alternative/alt_controller.py +++ b/openbb_terminal/alternative/alt_controller.py @@ -22,7 +22,7 @@ class AlternativeDataController(BaseController): """Alternative Controller class""" CHOICES_COMMANDS: List[str] = ["hn"] - CHOICES_MENUS = ["covid", "oss", "realestate"] + CHOICES_MENUS = ["covid", "oss", "realestate", "companieshouse"] PATH = "/alternative/" CHOICES_GENERATION = True @@ -41,6 +41,7 @@ class AlternativeDataController(BaseController): mt.add_menu("covid") mt.add_menu("oss") mt.add_menu("realestate") + mt.add_menu("companieshouse") mt.add_raw("\n") mt.add_cmd("hn") console.print(text=mt.menu_text, menu="Alternative") @@ -94,3 +95,12 @@ class AlternativeDataController(BaseController): ) self.queue = self.load_class(RealEstateController, self.queue) + + @log_start_end(log=logger) + def call_companieshouse(self, _): + """Process companieshouse command.""" + from openbb_terminal.alternative.companieshouse.companieshouse_controller import ( + CompaniesHouseController, + ) + + self.queue = self.load_class(CompaniesHouseController, self.queue) diff --git a/openbb_terminal/alternative/companieshouse/companieshouse_controller.py b/openbb_terminal/alternative/companieshouse/companieshouse_controller.py new file mode 100644 index 00000000000..40353ba5b00 --- /dev/null +++ b/openbb_terminal/alternative/companieshouse/companieshouse_controller.py @@ -0,0 +1,337 @@ +"""Companies House Controller.""" +__docformat__ = "numpy" + +import argparse +import logging +from typing import List, Optional + +from openbb_terminal.alternative.companieshouse import companieshouse_view +from openbb_terminal.core.session.current_user import get_current_user +from openbb_terminal.custom_prompt_toolkit import NestedCompleter +from openbb_terminal.decorators import check_api_key, log_start_end +from openbb_terminal.helper_funcs import ( + EXPORT_ONLY_RAW_DATA_ALLOWED, + check_positive, +) +from openbb_terminal.menu import session +from openbb_terminal.parent_classes import BaseController +from openbb_terminal.rich_config import MenuText, console + +logger = logging.getLogger(__name__) + + +class CompaniesHouseController(BaseController): + + """Companies House Controller class.""" + + CHOICES_COMMANDS = [ + "search", + "load", + "officers", + "signifcontrol", + "filings", + "filingdocument", + "charges", + ] + PATH = "/alternative/companieshouse/" + CHOICES_GENERATION = True + + def __init__(self, queue: Optional[List[str]] = None): + """Construct Data.""" + super().__init__(queue) + + self.companyNo = "" + self.companyName = "" + self.filingCategory = "" + self.filing_total_count = 0 + self.filing_end_index = 0 + self.filing_start_index = 0 + if session and get_current_user().preferences.USE_PROMPT_TOOLKIT: + choices: dict = self.choices_default + self.completer = NestedCompleter.from_nested_dict(choices) + + def print_help(self): + """Print help""" + company_string = ( + f"{self.companyNo} ({self.companyName})" if self.companyNo else "" + ) + + mt = MenuText("alternative/companieshouse/") + mt.add_param("_company", company_string) + mt.add_raw("\n") + mt.add_cmd("search") + mt.add_cmd("load") + mt.add_cmd("officers") + mt.add_cmd("signifcontrol") + mt.add_cmd("filings") + mt.add_cmd("filingdocument") + mt.add_cmd("charges") + + console.print(text=mt.menu_text, menu="UK Companies House Data") + + @log_start_end(log=logger) + @check_api_key(["API_COMPANIESHOUSE_KEY"]) + def call_search(self, other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="search", + description="Select the company name to search for. [Source: UK Companies House]", + ) + parser.add_argument( + "-n", + "--name", + help="name", + type=str.upper, + required="-h" not in other_args, + dest="name", + metavar="name", + nargs="+", + ) + + parser.add_argument( + "-l", + "--limit", + help="Number of entries to return", + type=check_positive, + required=False, + dest="limit", + metavar="limit", + default=20, + ) + + if ( + other_args + and "-n" not in other_args[0] + and "--name" not in other_args[0] + and "-h" not in other_args + ): + other_args.insert(0, "-n") + + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if ns_parser: + if ns_parser.name: + query = " ".join(ns_parser.name) + companieshouse_view.display_search( + query, ns_parser.limit, export=ns_parser.export + ) + else: + console.print("[red]No entries found for search string[/red]\n") + + @log_start_end(log=logger) + @check_api_key(["API_COMPANIESHOUSE_KEY"]) + def call_load(self, other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="load", + description="Select the company number to get detailed info on. [Source: UK Companies House]", + ) + parser.add_argument( + "-c", + "--companyNo", + help="companyNo", + type=str.upper, + required="-h" not in other_args, + dest="companyNo", + metavar="companyNo", + ) + + if ( + other_args + and "-c" not in other_args[0] + and "--companyNo" not in other_args[0] + and "-h" not in other_args + ): + other_args.insert(0, "-c") + + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if ns_parser and ns_parser.companyNo: + self.companyNo = ns_parser.companyNo + company = companieshouse_view.display_company_info( + ns_parser.companyNo, export=ns_parser.export + ) + if company.dataAvailable(): + self.companyName = company.name + self.filing_total_count = 0 + self.filing_end_index = 0 + console.print(company.name) + console.print(company.address) + console.print(company.lastAccounts) + else: + console.print( + f"[red]No data found for company number {ns_parser.companyNo}[/red]\n" + ) + + @log_start_end(log=logger) + @check_api_key(["API_COMPANIESHOUSE_KEY"]) + def call_officers(self, other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="officers", + description="Select the company number to retrieve officers for. [Source: UK Companies House]", + ) + + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if self.companyNo: + if ns_parser: + companieshouse_view.display_officers( + self.companyNo, export=ns_parser.export + ) + else: + console.print("Must load a company prior to using this command") + + @log_start_end(log=logger) + @check_api_key(["API_COMPANIESHOUSE_KEY"]) + def call_signifcontrol(self, other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="signifcontrol", + description="Select the company number to retrieve persons with significant control of company. \ + [Source: UK Companies House]", + ) + + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if self.companyNo: + if ns_parser: + companieshouse_view.display_persons_with_significant_control( + self.companyNo, export=ns_parser.export + ) + else: + console.print("Must load a company prior to using this command") + + @log_start_end(log=logger) + @check_api_key(["API_COMPANIESHOUSE_KEY"]) + def call_filings(self, other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="filings", + description="Select the company number to retrieve filling history for. [Source: UK Companies House]", + ) + + parser.add_argument( + "-k", + "--category", + help="category", + type=str.lower, + required=False, + dest="category", + metavar="category", + choices=[ + "accounts", + "address", + "capital", + "incorporation", + "officers", + "resolution", + ], + ) + + parser.add_argument( + "-l", + "--limit", + help="Number of entries to return", + type=check_positive, + required=False, + dest="limit", + metavar="limit", + default=100, + ) + + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if self.companyNo: + if ns_parser: + category = ns_parser.category if ns_parser.category else "" + self.filingCategory = category + filing_data = companieshouse_view.display_filings( + self.companyNo, category, ns_parser.limit, export=ns_parser.export + ) + self.filing_total_count = filing_data.total_count + self.filing_end_index = filing_data.end_index + self.filing_start_index = filing_data.start_index + else: + console.print("Must load a company prior to using this command") + + @log_start_end(log=logger) + @check_api_key(["API_COMPANIESHOUSE_KEY"]) + def call_filingdocument(self, other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="filingdocument", + description="Select the company number and transaction ID to retrieve filling history for. \ + [Source: UK Companies House]", + ) + + parser.add_argument( + "-t", + "--transactionID", + help="transactionID", + action="store", + required=("-h" not in other_args), + dest="transactionID", + metavar="transactionID", + ) + + if ( + other_args + and "-t" not in other_args[0] + and "--transactionID" not in other_args[0] + and "-h" not in other_args + ): + other_args.insert(0, "-t") + + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if self.companyNo: + if ns_parser: + companieshouse_view.download_filing_document( + self.companyNo, + self.companyName, + ns_parser.transactionID, + export=ns_parser.export, + ) + else: + console.print("Must load a company prior to using this command") + + @log_start_end(log=logger) + @check_api_key(["API_COMPANIESHOUSE_KEY"]) + def call_charges(self, other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="charges", + description="Select the company number to retrieve officers for. [Source: UK Companies House]", + ) + + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if self.companyNo: + if ns_parser: + companieshouse_view.display_charges( + self.companyNo, export=ns_parser.export + ) + else: + console.print("Must load a company prior to using this command") diff --git a/openbb_terminal/alternative/companieshouse/companieshouse_model.py b/openbb_terminal/alternative/companieshouse/companieshouse_model.py new file mode 100644 index 00000000000..462553956f8 --- /dev/null +++ b/openbb_terminal/alternative/companieshouse/companieshouse_model.py @@ -0,0 +1,481 @@ +""" UK Companies House Model """ +__docformat__ = "numpy" + +import logging + +import pandas as pd +import requests + +from openbb_terminal.alternative.companieshouse.company import Company +from openbb_terminal.alternative.companieshouse.company_doc import CompanyDocument +from openbb_terminal.alternative.companieshouse.filing_data import Filing_data +from openbb_terminal.core.session.constants import ( + TIMEOUT, +) +from openbb_terminal.core.session.current_user import get_current_user +from openbb_terminal.decorators import check_api_key, log_start_end + +logger = logging.getLogger(__name__) + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_search_results(searchStr: str, limit: int = 20) -> pd.DataFrame: + """All companies with searchStr in their name. + + Parameters + ---------- + searchStr : str + The search string + limit : int + number of rows to return + + Returns + ------- + pd.DataFrame + All comapanies with the search string in their name. + + Example + ------- + >>> from openbb_terminal.sdk import openbb + >>> companies = openbb.alt.companieshouse.get_search_results("AstraZeneca") + """ + + df = pd.DataFrame() + + if not searchStr: + return df + + auth = requests.auth.HTTPBasicAuth( + get_current_user().credentials.API_COMPANIESHOUSE_KEY, "" + ) + r = requests.get( + "https://api.company-information.service.gov.uk/search/companies?q=" + + searchStr + + f"&items_per_page={limit}", + auth=auth, + timeout=TIMEOUT, + ) + returned_data = r.json() + company_data = [] + for index, item in enumerate(returned_data["items"]): + company_data.append( + { + "Name": item["title"], + "Company Number": item["company_number"], + "Status": item["company_status"], + } + ) + + df = pd.DataFrame(company_data) + return df + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_company_info(company_number: str) -> Company: + """Gets company info by company number + + Parameters + ---------- + company_number : str + The company number. Use get_search_results() to lookup company numbers. + + Returns + ------- + self.address: str + Company address. + self.name: str + Company name. + self.dataAvailable(): bool + True if data is available. + self.lastAccounts: str + Period start and end. + + Example + ------- + >>> companies = openbb.alt.companieshouse.get_search_results("AstraZeneca") + >>> company_info = openbb.alt.companieshouse.get_company_info("02723534") + >>> name = company_info.name + """ + + auth = requests.auth.HTTPBasicAuth( + get_current_user().credentials.API_COMPANIESHOUSE_KEY, "" + ) + r = requests.get( + f"https://api.company-information.service.gov.uk/company/{company_number}", + auth=auth, + timeout=TIMEOUT, + ) + + last_accounts = {} + returned_data = r.json() + if returned_data.get("company_name"): + company_name = returned_data["company_name"] + if returned_data.get("accounts"): + last_accounts = returned_data["accounts"]["last_accounts"] + address = returned_data["registered_office_address"] + address_lines = [] + if address.get("address_line_1"): + address_lines.append(address.get("address_line_1")) + if address.get("address_line_2"): + address_lines.append(address.get("address_line_2")) + if address.get("locality"): + address_lines.append(address.get("locality")) + if address.get("region"): + address_lines.append(address.get("region")) + if address.get("postal_code"): + address_lines.append(address.get("postal_code")) + pretty_address = ( + ",".join(address_lines) + if len(address_lines) > 0 + else "No address data found" + ) + + if last_accounts: + pretty_accounts = "Period Start On : " + ( + last_accounts.get("period_start_on") or "" + ) + " - " + "Type : " + ( + last_accounts.get("type") or "" + ) + " - " + "Made Up To : " + ( + last_accounts.get("made_up_to") or "" + ) + " - " "Period End On : " + ( + last_accounts.get("period_end_on") or "" + ) + else: + pretty_accounts = "No accounting period data found" + + data = Company(company_name, pretty_address, pretty_accounts) + ret |