From b5bc4c77218f60b6942ddd855d12310ec1a95718 Mon Sep 17 00:00:00 2001 From: dio Date: Mon, 28 Aug 2023 15:28:33 +0100 Subject: [PATCH] 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 --- openbb_terminal/alternative/alt_controller.py | 12 +- .../companieshouse_controller.py | 337 ++++++++++++ .../companieshouse/companieshouse_model.py | 481 ++++++++++++++++++ .../companieshouse/companieshouse_view.py | 253 +++++++++ .../alternative/companieshouse/company.py | 14 + .../alternative/companieshouse/company_doc.py | 31 ++ .../alternative/companieshouse/filing_data.py | 19 + openbb_terminal/core/config/paths_helper.py | 1 + .../core/models/preferences_model.py | 1 + .../sdk/controllers/alt_sdk_controller.py | 17 + .../core/sdk/models/alt_sdk_model.py | 37 ++ openbb_terminal/core/sdk/sdk_init.py | 2 + openbb_terminal/core/sdk/trail_map.csv | 8 + openbb_terminal/keys_controller.py | 30 ++ openbb_terminal/keys_model.py | 77 +++ openbb_terminal/miscellaneous/i18n/en.yml | 15 +- .../miscellaneous/models/hub_credentials.json | 1 + openbb_terminal/sdk.py | 1 + .../test_companieshouse_controller.py | 172 +++++++ .../test_companieshouse_model.py | 299 +++++++++++ .../test_companieshouse_view.py | 283 +++++++++++ .../test_print_help.txt | 10 + .../test_display_charges.txt | 11 + .../test_display_filings.txt | 5 + .../test_display_officers.txt | 3 + ...splay_persons_with_significant_control.txt | 3 + .../test_display_search.txt | 4 + .../test_alt_controller/test_print_help.txt | 7 +- website/content/sdk/usage/guides/api-keys.md | 28 +- .../content/terminal/usage/guides/api-keys.md | 23 + 30 files changed, 2174 insertions(+), 11 deletions(-) create mode 100644 openbb_terminal/alternative/companieshouse/companieshouse_controller.py create mode 100644 openbb_terminal/alternative/companieshouse/companieshouse_model.py create mode 100644 openbb_terminal/alternative/companieshouse/companieshouse_view.py create mode 100644 openbb_terminal/alternative/companieshouse/company.py create mode 100644 openbb_terminal/alternative/companieshouse/company_doc.py create mode 100644 openbb_terminal/alternative/companieshouse/filing_data.py create mode 100644 tests/openbb_terminal/alternative/companieshouse/test_companieshouse_controller.py create mode 100644 tests/openbb_terminal/alternative/companieshouse/test_companieshouse_model.py create mode 100644 tests/openbb_terminal/alternative/companieshouse/test_companieshouse_view.py create mode 100644 tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_controller/test_print_help.txt create mode 100644 tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_charges.txt create mode 100644 tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_filings.txt create mode 100644 tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_officers.txt create mode 100644 tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_persons_with_significant_control.txt create mode 100644 tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_search.txt diff --git a/openbb_terminal/alternative/alt_controller.py b/openbb_terminal/alternative/alt_controller.py index cc4e7e1713d9..4352e8193df5 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 @@ def print_help(self): 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 @@ def call_realestate(self, _): ) 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 000000000000..40353ba5b003 --- /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 000000000000..462553956f89 --- /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) + return data + else: + return Company() + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_officers(company_number: str) -> pd.DataFrame: + """Gets information on company officers + + Parameters + ---------- + company_number : str + The company number. Use get_search_results() to lookup company numbers. + + Returns + ------- + pd.Dataframe + All officers for given company number + + Example + ------- + >>> companies = openbb.alt.companieshouse.get_search_results("AstraZeneca") + >>> officer_info = openbb.alt.companieshouse.get_officers("02723534") + """ + + 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}/officers?items_per_page=100", + auth=auth, + timeout=TIMEOUT, + ) + returned_data = r.json() + + officers = [] + if returned_data.get("items"): + for index, item in enumerate(returned_data["items"]): + officers.append( + { + "Officer Role": (item.get("officer_role") or " - "), + "Appointed On": (item.get("appointed_on") or " - "), + "Resigned On": (item.get("resigned_on") or " - "), + "Name": (item.get("name") or " - "), + } + ) + + df = pd.DataFrame(officers) + + return df + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_persons_with_significant_control(company_number: str) -> pd.DataFrame: + """Gets information on persons with significant control over the company + + Parameters + ---------- + company_number : str + The company number. Use get_search_results() to lookup company numbers. + + Returns + ------- + pd.Dataframe + All persons with significant control over given company number + + Example + ------- + >>> companies = openbb.alt.companieshouse.get_search_results("AstraZeneca") + >>> signif_control_info = openbb.alt.companieshouse.get_persons_with_significant_control("02723534") + """ + + 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}/persons-with-significant-control?items_per_page=100", + auth=auth, + timeout=TIMEOUT, + ) + returned_data = r.json() + + controllers = [] + if returned_data.get("items"): + for index, item in enumerate(returned_data["items"]): + controllers.append( + { + "Kind": (item.get("kind") or " - "), + "Name": (item.get("name") or " - "), + "Natures of Control": (item.get("natures_of_control") or " - "), + "Notified On": (item.get("notified_on") or " - "), + } + ) + + df = pd.DataFrame(controllers) + + return pd.DataFrame(df) + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_filings(company_number: str, category: str = "", start_index=0) -> Filing_data: + """Gets information on filings for given company, e.g. accounts, etc + + Parameters + ---------- + company_number : str + The company number. Use get_search_results() to lookup company numbers. + + Returns + ------- + pd.Dataframe + All information on all filings for company + + Example + ------- + >>> companies = openbb.alt.companieshouse.get_search_results("AstraZeneca") + >>> signif_control_info = openbb.alt.companieshouse.get_filings("02723534") + """ + + auth = requests.auth.HTTPBasicAuth( + get_current_user().credentials.API_COMPANIESHOUSE_KEY, "" + ) + url = ( + f"https://api.company-information.service.gov.uk/company/{company_number}/filing-history" + + f"?start_index={start_index}&items_per_page=100" + ) + if category: + url = ( + f"https://api.company-information.service.gov.uk/company/{company_number}/filing-history" + + f"?category={category}&start_index={start_index}&items_per_page=100" + ) + + r = requests.get( + url, + auth=auth, + timeout=TIMEOUT, + ) + returned_data = r.json() + + filings = [] + for index, item in enumerate(returned_data["items"]): + filings.append( + { + "Category": (item.get("category") or " - "), + "Date": (item.get("date") or " - "), + "Description": (item.get("description") or " - "), + "Type": (item.get("type") or " - "), + "Pages": (item.get("pages") or " - "), + "Transaction ID": (item.get("transaction_id") or " - "), + "Download": "https://find-and-update.company-information.service.gov.uk/company/" + f"{company_number}" + "/filing-history/" + f"{item.get('transaction_id')}" + "/document?format=pdf&download=0", + } + ) + start_index = int(returned_data.get("start_index")) + total_count = int(returned_data.get("total_count")) + end_index = start_index + 100 + if end_index > total_count: + end_index = total_count + data = Filing_data(pd.DataFrame(filings), start_index, end_index, total_count) + + return data + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_filing_document(company_number: str, transactionID: str) -> CompanyDocument: + """Download given filing document pdf + + Parameters + ---------- + company_number : str + The company number. Use get_search_results() to lookup company numbers. + transactionID : str + The filing transaction id. Use get_filings() to get id for each document + + Returns + ------- + self.category: str + document category, e.g.confirmation-statement-with-updates, accounts-with-accounts-type-dormant, etc. + self.date: str + date document filed. + self.description: str + description of document files. + self.paper_filed: str + True if documents filed in paper format. + self.pages: str + Number of pages in document. + self.transaction_id: str + Document transaction id. + self.content: bytes + contents of the document. + + Example + ------- + >>> companies = openbb.alt.companieshouse.get_search_results("AstraZeneca") + >>> company_doc_info = openbb.alt.companieshouse.get_filing_document("02723534","MzM1NzQ0NzI5NWFkaXF6a2N4") + """ + + 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}/filing-history/{transactionID}", + auth=auth, + timeout=TIMEOUT, + ) + + returned_data = r.json() + + category = date = description = paper_filed = pages = transaction_id = "" + content = b"" + if returned_data.get("links") and returned_data.get("links").get( + "document_metadata" + ): + url = returned_data.get("links").get("document_metadata") + "/content" + response = requests.get( + url, auth=auth, headers={"Accept": "application/pdf"}, timeout=TIMEOUT + ) + + if returned_data.get("category"): + category = returned_data["category"] + if returned_data.get("date"): + date = returned_data["date"] + if returned_data.get("description"): + description = returned_data["description"] + if returned_data.get("paper_filed"): + paper_filed = returned_data["paper_filed"] + if returned_data.get("pages"): + pages = returned_data["pages"] + if returned_data.get("transaction_id"): + transaction_id = returned_data["transaction_id"] + + content = bytes(response.content) + + return CompanyDocument( + category, date, description, paper_filed, pages, transaction_id, content + ) + else: + return CompanyDocument( + category, date, description, paper_filed, pages, transaction_id, content + ) + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_charges(company_number: str) -> pd.DataFrame: + 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}/charges", + auth=auth, + timeout=TIMEOUT, + ) + + returned_data = r.json() + + charges = pd.DataFrame() + for index, item in enumerate(returned_data["items"]): + url = item.get("links").get("self") + id = url[url.rfind("/") + 1 :] + charges = pd.concat( + [charges, get_charge(company_number, id)], ignore_index=True + ) + + return charges + + +@log_start_end(log=logger) +@check_api_key(["API_COMPANIESHOUSE_KEY"]) +def get_charge(company_number: str, charge_id: str) -> pd.DataFrame: + 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}/charges/{charge_id}", + auth=auth, + timeout=TIMEOUT, + ) + + returned_data = r.json() + + charge = {} + if returned_data.get("acquired_on"): + charge.update({"acquired_on": returned_data.get("acquired_on")}) + if returned_data.get("assests_ceased_released"): + charge.update( + {"assests_ceased_released": returned_data.get("assests_ceased_released")} + ) + + if returned_data.get("charge_number"): + charge.update({"charge_number": returned_data.get("charge_number")}) + + if returned_data.get("covering_instrument_date"): + charge.update( + {"covering_instrument_date": returned_data.get("covering_instrument_date")} + ) + + if returned_data.get("created_on"): + charge.update({"created_on": returned_data.get("created_on")}) + + if returned_data.get("delivered_on"): + charge.update({"delivered_on": returned_data.get("delivered_on")}) + + if returned_data.get("id"): + charge.update({"id": returned_data.get("id")}) + + if returned_data.get("status"): + charge.update({"status": returned_data.get("status")}) + + if returned_data.get("particulars"): + part = returned_data.get("particulars") + + if part.get("description"): + charge.update({"description": part.get("description")}) + if part.get("floating_charge_covers_all"): + charge.update( + {"floating_charge_covers_all": part.get("floating_charge_covers_all")} + ) + if part.get("type"): + charge.update({"type": part.get("type")}) + + if returned_data.get("persons_entitled"): + for entitled in returned_data.get("persons_entitled"): + charge.update({"persons_entitled_name": entitled.get("name")}) + + return pd.DataFrame([charge]) diff --git a/openbb_terminal/alternative/companieshouse/companieshouse_view.py b/openbb_terminal/alternative/companieshouse/companieshouse_view.py new file mode 100644 index 000000000000..d7e88ad69a6a --- /dev/null +++ b/openbb_terminal/alternative/companieshouse/companieshouse_view.py @@ -0,0 +1,253 @@ +""" UK Companies House View """ +__docformat__ = "numpy" + +import logging +import os + +import pandas as pd + +from openbb_terminal.alternative.companieshouse import companieshouse_model +from openbb_terminal.alternative.companieshouse.company import Company +from openbb_terminal.alternative.companieshouse.filing_data import Filing_data +from openbb_terminal.core.session.current_user import get_current_user +from openbb_terminal.decorators import log_start_end +from openbb_terminal.helper_funcs import export_data, print_rich_table +from openbb_terminal.rich_config import console + +logger = logging.getLogger(__name__) + + +@log_start_end(log=logger) +def display_search(search_str: str, limit: int, export: str = "") -> None: + """Display company search results. + + Parameters + ---------- + search_str : str + Company name to search for + + limit : int + number of rows to return + + """ + results = companieshouse_model.get_search_results(search_str, limit) + + if results.empty or len(results) == 0: + console.print( + "[red]" + "No data loaded.\n" + "Try different search string." + "[/red]\n" + ) + return + + console.print(f"Retrieved {len(results)} records") + print_rich_table( + results, + show_index=False, + title=f"[bold]{search_str}[/bold]", + export=bool(export), + columns_keep_types=["Company Number"], + ) + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "results", + results, + ) + + +@log_start_end(log=logger) +def display_company_info(company_number: str, export: str = "") -> Company: + """Display company search results. + + Parameters + ---------- + company_number : str + company_number to retrieve info for + + + """ + return companieshouse_model.get_company_info(company_number) + + +@log_start_end(log=logger) +def display_officers(company_number: str, export: str = "") -> None: + """Display company officers results. + + Parameters + ---------- + company_number : str + company_number to retrieve officers for + + + """ + results = companieshouse_model.get_officers(company_number) + + if len(results) > 0: + console.print(f"Retrieved {len(results)} records") + print_rich_table( + results, + show_index=False, + title=f"[bold]{company_number}[/bold]", + export=bool(export), + ) + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "results", + results, + ) + else: + console.print("[red]No Data Found[/red]") + + +@log_start_end(log=logger) +def display_persons_with_significant_control( + company_number: str, export: str = "" +) -> None: + """Display company officers results. + + Parameters + ---------- + company_number : str + company_number to retrieve officers for + + + """ + results = companieshouse_model.get_persons_with_significant_control(company_number) + + if len(results) > 0: + console.print(f"Retrieved {len(results)} records") + print_rich_table( + results, + show_index=False, + title=f"[bold]{company_number}[/bold]", + export=bool(export), + ) + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "results", + results, + ) + else: + console.print("[red]No Data Found[/red]") + + +@log_start_end(log=logger) +def display_filings( + company_number: str, + category: str = "", + limit: int = 100, + export: str = "", +) -> Filing_data: + """Display company's filing history. + + Parameters + ---------- + company_number : str + company_number to retrieve filing history for + + """ + start_index = 0 + results = companieshouse_model.get_filings(company_number, category, start_index) + + start = int(results.start_index) + + data = results + total = int(results.total_count) if int(results.total_count) < limit else limit + + while start < total - 100: + results = companieshouse_model.get_filings( + company_number, start_index=start + 100 + ) + data.filings = pd.concat([data.filings, results.filings], ignore_index=True) + start = start + 100 + + data.filings = data.filings.head(limit) + console.print(f"Retrieved {len(data.filings)} filings") + + print_rich_table( + data.filings, + show_index=False, + title=f"[bold]{company_number}[/bold]", + export=bool(export), + ) + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "results", + data.filings, + ) + + return results + + +def download_filing_document( + company_number: str, company_name: str, transactionID: str, export: str = "" +) -> None: + """Download company's filing document. + + Parameters + ---------- + company_number : str + company_number to retrieve filing history for + + company_name : str + company_name to retrieve filing document for, this is used to name the downloaded file for easy access + + transactionID : str + transaction id for filing + + >>> companies = openbb.alt.companieshouse.get_search_results("AstraZeneca") + >>> openbb.alt.companieshouse.get_filing_document("02723534","AstraZeneca","MzM1NzQ0NzI5NWFkaXF6a2N4") + """ + + results = companieshouse_model.get_filing_document(company_number, transactionID) + + if results.dataAvailable(): + filename = ( + company_name.replace(" ", "_") + + "_" + + results.category + + "_" + + transactionID + + ".pdf" + ) + with open( + f"{get_current_user().preferences.USER_COMPANIES_HOUSE_DIRECTORY}/{filename}", + "wb", + ) as f: + f.write(results.content) + console.print( + f"File [green] {filename}[/green] downloaded to \ + {get_current_user().preferences.USER_COMPANIES_HOUSE_DIRECTORY}" + ) + else: + console.print("[red]" + "Document not found" + "[/red]\n") + + +@log_start_end(log=logger) +def display_charges(company_number: str, export: str = "") -> None: + results = companieshouse_model.get_charges(company_number) + + if len(results) > 0: + print_rich_table( + results, + show_index=False, + title=f"[bold]{company_number}[/bold]", + export=bool(export), + ) + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "results", + results, + ) + else: + console.print("[red]" + "No Charges found" + "[/red]\n") + + return results diff --git a/openbb_terminal/alternative/companieshouse/company.py b/openbb_terminal/alternative/companieshouse/company.py new file mode 100644 index 000000000000..c4e0ae1f1260 --- /dev/null +++ b/openbb_terminal/alternative/companieshouse/company.py @@ -0,0 +1,14 @@ +class Company: + """Holder class for company information""" + + name = "" + address = "" + lastAccounts = "" + + def __init__(self, name="", address: str = "", lastAccounts: str = ""): + self.name = name + self.address = address + self.lastAccounts = lastAccounts + + def dataAvailable(self) -> bool: + return len(self.name) > 0 diff --git a/openbb_terminal/alternative/companieshouse/company_doc.py b/openbb_terminal/alternative/companieshouse/company_doc.py new file mode 100644 index 000000000000..2b473989288c --- /dev/null +++ b/openbb_terminal/alternative/companieshouse/company_doc.py @@ -0,0 +1,31 @@ +class CompanyDocument: + """Holder class for company document information""" + + category = "" + date = "" + description = "" + paper_filed = "" + pages = "" + transaction_id = "" + content = b"" + + def __init__( + self, + category="", + date: str = "", + description: str = "", + paper_filed: str = "", + pages: str = "", + transaction_id: str = "", + content=b"", + ): + self.category = category + self.date = date + self.description = description + self.paper_filed = paper_filed + self.pages = pages + self.transaction_id = transaction_id + self.content = content + + def dataAvailable(self) -> bool: + return len(self.content) > 0 diff --git a/openbb_terminal/alternative/companieshouse/filing_data.py b/openbb_terminal/alternative/companieshouse/filing_data.py new file mode 100644 index 000000000000..d6b7da7f8936 --- /dev/null +++ b/openbb_terminal/alternative/companieshouse/filing_data.py @@ -0,0 +1,19 @@ +import pandas as pd + + +class Filing_data: + """Holder class for company filings information""" + + filings = pd.DataFrame() + total_count = 0 + start_index = 0 + end_index = 0 + + def __init__(self, filings, start_index, end_index, total_count): + self.filings = filings + self.start_index = start_index + self.end_index = end_index + self.total_count = total_count + + def dataAvailable(self) -> bool: + return len(self.filings) > 0 diff --git a/openbb_terminal/core/config/paths_helper.py b/openbb_terminal/core/config/paths_helper.py index 3f5dad67a6c6..0a5a08c18e53 100644 --- a/openbb_terminal/core/config/paths_helper.py +++ b/openbb_terminal/core/config/paths_helper.py @@ -42,6 +42,7 @@ def create_files(list_files: List): current_user.preferences.USER_DATA_DIRECTORY / "styles", current_user.preferences.USER_DATA_DIRECTORY / "reports", current_user.preferences.USER_DATA_DIRECTORY / "reports" / "custom reports", + current_user.preferences.USER_DATA_DIRECTORY / "companies_house", current_user.preferences.USER_CUSTOM_IMPORTS_DIRECTORY, current_user.preferences.USER_CUSTOM_IMPORTS_DIRECTORY / "econometrics", current_user.preferences.USER_CUSTOM_IMPORTS_DIRECTORY / "stocks", diff --git a/openbb_terminal/core/models/preferences_model.py b/openbb_terminal/core/models/preferences_model.py index e92e855eb353..86c43bd954f4 100644 --- a/openbb_terminal/core/models/preferences_model.py +++ b/openbb_terminal/core/models/preferences_model.py @@ -85,6 +85,7 @@ class PreferencesModel(BaseModel): USER_FORECAST_MODELS_DIRECTORY = USER_DATA_DIRECTORY / "exports" / "forecast_models" USER_FORECAST_WHISPER_DIRECTORY = USER_DATA_DIRECTORY / "exports" / "whisper" USER_STYLES_DIRECTORY = USER_DATA_DIRECTORY / "styles" + USER_COMPANIES_HOUSE_DIRECTORY = USER_DATA_DIRECTORY / "companies_house" def __repr__(self) -> str: # pylint: disable=useless-super-delegation return super().__repr__() diff --git a/openbb_terminal/core/sdk/controllers/alt_sdk_controller.py b/openbb_terminal/core/sdk/controllers/alt_sdk_controller.py index 4fa0bb70289c..478cf6fb7e62 100644 --- a/openbb_terminal/core/sdk/controllers/alt_sdk_controller.py +++ b/openbb_terminal/core/sdk/controllers/alt_sdk_controller.py @@ -11,6 +11,7 @@ class AltController(model.AltRoot): `covid`: Covid Module `oss`: Oss Module `realestate`: Realestate Module + `companieshouse`: CompaniesHouse Module Attributes: `hn`: Get top stories from HackerNews.\n @@ -66,3 +67,19 @@ def realestate(self): """ return model.AltRealestate() + + @property + def companieshouse(self): + """Alternative CompaniesHouse Submodule + + Attributes: + `get_search_results`: Search for companies by name\n + `get_company_info`: Get details of company by registration number\n + `get_officers`: Get company officers\n + `get_persons_with_significant_control`: Get people/organisations with significant control over company\n + `get_filings`: Get company filing details\n + `get_filing_document`: Get company filed document\n + `download_filing_document`: Download company filed document\n + """ + + return model.AltCompaniesHouse() diff --git a/openbb_terminal/core/sdk/models/alt_sdk_model.py b/openbb_terminal/core/sdk/models/alt_sdk_model.py index cc77efb4f5e6..1aa4781dc4db 100644 --- a/openbb_terminal/core/sdk/models/alt_sdk_model.py +++ b/openbb_terminal/core/sdk/models/alt_sdk_model.py @@ -103,3 +103,40 @@ def __init__(self): self.get_towns_sold_prices = ( lib.alt_realestate_landRegistry_model.get_towns_sold_prices ) + + +class AltCompaniesHouse(Category): + """CompaniesHouse Module. + + Attributes: + `get_search_results`: Search for company by name\n + `get_company_info`: Get details of company by registration number\n + `get_officers`: Get company officers\n + `get_persons_with_significant_control`: Get people/organisations with significant control over company\n + `get_charges`: Get company charges\n + `get_filings`: Get company filing details\n + `get_filing_document`: Get company filed document\n + """ + + _location_path = "alt.companieshouse" + + def __init__(self): + super().__init__() + self.get_search_results = ( + lib.alt_companieshouse_companieshouse_model.get_search_results + ) + self.get_company_info = ( + lib.alt_companieshouse_companieshouse_model.get_company_info + ) + self.get_officers = lib.alt_companieshouse_companieshouse_model.get_officers + self.get_persons_with_significant_control = ( + lib.alt_companieshouse_companieshouse_model.get_persons_with_significant_control + ) + self.get_charges = lib.alt_companieshouse_companieshouse_model.get_charges + self.get_filings = lib.alt_companieshouse_companieshouse_model.get_filings + self.get_filing_document = ( + lib.alt_companieshouse_companieshouse_model.get_filing_document + ) + self.download_filing_document = ( + lib.alt_companieshouse_companieshouse_view.download_filing_document + ) diff --git a/openbb_terminal/core/sdk/sdk_init.py b/openbb_terminal/core/sdk/sdk_init.py index e075c597bb22..2396d8ceb3c8 100644 --- a/openbb_terminal/core/sdk/sdk_init.py +++ b/openbb_terminal/core/sdk/sdk_init.py @@ -2,6 +2,8 @@ # noqa: F401 # Alternative +import openbb_terminal.alternative.companieshouse.companieshouse_model as alt_companieshouse_companieshouse_model +import openbb_terminal.alternative.companieshouse.companieshouse_view as alt_companieshouse_companieshouse_view import openbb_terminal.alternative.hackernews_model as alt_hackernews_model import openbb_terminal.alternative.hackernews_view as alt_hackernews_view import openbb_terminal.alternative.oss.github_model as alt_oss_github_model diff --git a/openbb_terminal/core/sdk/trail_map.csv b/openbb_terminal/core/sdk/trail_map.csv index ebb70a1affac..02a7223d87c6 100644 --- a/openbb_terminal/core/sdk/trail_map.csv +++ b/openbb_terminal/core/sdk/trail_map.csv @@ -16,6 +16,14 @@ alt.oss.top,alt_oss_github_model.get_top_repos,alt_oss_github_view.display_top_r alt.realestate.get_estate_sales,alt_realestate_landRegistry_model.get_estate_sales, alt.realestate.get_region_stats,alt_realestate_landRegistry_model.get_region_stats, alt.realestate.get_towns_sold_prices,alt_realestate_landRegistry_model.get_towns_sold_prices, +alt.companieshouse.get_search_results,alt_companieshouse_companieshouse_model.get_search_results, +alt.companieshouse.get_company_info,alt_companieshouse_companieshouse_model.get_company_info, +alt.companieshouse.get_officers,alt_companieshouse_companieshouse_model.get_officers, +alt.companieshouse.get_persons_with_significant_control,alt_companieshouse_companieshouse_model.get_persons_with_significant_control, +alt.companieshouse.get_filings,alt_companieshouse_companieshouse_model.get_filings, +alt.companieshouse.get_filing_document,alt_companieshouse_companieshouse_model.get_filing_document, +alt.companieshouse.download_filing_document,alt_companieshouse_companieshouse_view.download_filing_document, +alt.companieshouse.get_charges,alt_companieshouse_companieshouse_view.display_charges crypto.candle,crypto_helpers.plot_candles, crypto.chart,crypto_helpers.plot_chart, crypto.dd.active,crypto_dd_glassnode_model.get_active_addresses,crypto_dd_glassnode_view.display_active_addresses diff --git a/openbb_terminal/keys_controller.py b/openbb_terminal/keys_controller.py index 558faf4c014e..b9d810230281 100644 --- a/openbb_terminal/keys_controller.py +++ b/openbb_terminal/keys_controller.py @@ -1281,6 +1281,36 @@ def call_dappradar(self, other_args: List[str]): key=ns_parser.key, persist=True, show_output=True ) + @log_start_end(log=logger) + def call_companieshouse(self, other_args: List[str]): + """Process companies house command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="companieshouse", + description="Set Companies House API key.", + ) + parser.add_argument( + "-k", + "--key", + type=str, + dest="key", + help="key", + ) + if not other_args: + console.print( + "For your API Key, visit: https://developer.company-information.service.gov.uk/overview" + ) + return + + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-k") + ns_parser = self.parse_simple_args(parser, other_args) + if ns_parser: + self.status_dict["companieshouse"] = keys_model.set_companieshouse_key( + key=ns_parser.key, persist=True, show_output=True + ) + @log_start_end(log=logger) def call_nixtla(self, other_args: List[str]): """Process nixtla command""" diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 7e62daa9b99c..45af3419a008 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -83,6 +83,7 @@ "tokenterminal": "TOKEN_TERMINAL", "stocksera": "STOCKSERA", "dappradar": "DAPPRADAR", + "companieshouse": "COMPANIES_HOUSE", "openai": "OPENAI", "nixtla": "NIXTLA", } @@ -2750,6 +2751,82 @@ def check_dappradar_key(show_output: bool = False) -> str: return str(status) +def set_companieshouse_key( + key: str, + persist: bool = False, + show_output: bool = False, +) -> str: + """Set Companies House key + + Parameters + ---------- + key: str + API ID + + persist: bool, optional + If False, api key change will be contained to where it was changed. For example, a Jupyter notebook session. + If True, api key change will be global, i.e. it will affect terminal environment variables. + By default, False. + show_output: bool, optional + Display status string or not. By default, False. + + Returns + ------- + str + Status of key set + + Examples + -------- + >>> from openbb_terminal.sdk import openbb + >>> openbb.keys.companieshouse( + api_id="example_id", + ) + """ + + handle_credential("API_COMPANIESHOUSE_KEY", key, persist) + return check_companieshouse_key(show_output) + + +def check_companieshouse_key(show_output: bool = False) -> str: + """Check Companies House key + + Parameters + ---------- + show_output: bool + Display status string or not. By default, False. + + Returns + ------- + str + Status of key set + """ + + current_user = get_current_user() + + if current_user.credentials.API_COMPANIESHOUSE_KEY == "REPLACE_ME": + logger.info("Companies House key not defined") + status = KeyStatus.NOT_DEFINED + else: + r = request( + "https://api.company-information.service.gov.uk/company/00000118", + auth=(f"{current_user.credentials.API_COMPANIESHOUSE_KEY}", ""), + ) + if r.status_code in [403, 401, 429]: + logger.warning("Companies House key defined, test failed") + status = KeyStatus.DEFINED_TEST_FAILED + elif r.status_code == 200: + logger.info("Companies House key defined, test passed") + status = KeyStatus.DEFINED_TEST_PASSED + else: + logger.warning("Companies House key defined, test inconclusive") + status = KeyStatus.DEFINED_TEST_INCONCLUSIVE + + if show_output: + console.print(status.colorize()) + + return str(status) + + # Set OpenAI key def set_openai_key(key: str, persist: bool = False, show_output: bool = False) -> str: """Set OpenAI key diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 8c229af0b873..9197ef8f0d0a 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -921,9 +921,10 @@ en: funds/exclusion: exclusion policy funds/_country_ms: Current Country MorningStar alternative/hn: Hacker News most popular stories - alternative/covid: COVID menu cases, deaths, rates - alternative/oss: Open Source menu star history, repos information - alternative/realestate: Real Estate menu sold prices, HPIs + alternative/covid: COVID menu, cases, deaths, rates + alternative/oss: Open Source menu, star history, repos information + alternative/realestate: Real Estate menu, sold prices, HPIs + alternative/companieshouse: Companies House menu, UK company information alternative/covid/slopes: get countries with highest slope in cases alternative/covid/country: select country for data alternative/covid/_country: Country @@ -943,6 +944,14 @@ en: alternative/realestate/_enddate: End Date alternative/realestate/regionstats: retrieve UK sold house price stats by region alternative/realestate/_region: Region + alternative/companieshouse/_company: Current Company + alternative/companieshouse/search: search for companies by name + alternative/companieshouse/load: retrieve basic company details by company registration number + alternative/companieshouse/officers: retrieve company officies + alternative/companieshouse/signifcontrol: retrieve those with significant control over the company + alternative/companieshouse/filings: retrieve list of documents filed with Companies House (Default 100) + alternative/companieshouse/filingdocument: download filed document + alternative/companieshouse/charges: registered charges against the company econometrics/_data_loc: Looking for data in econometrics/load: load a dataset (also works with StatsModels datasets) econometrics/export: export a processed dataset diff --git a/openbb_terminal/miscellaneous/models/hub_credentials.json b/openbb_terminal/miscellaneous/models/hub_credentials.json index a3ca194184a7..10bdc74c068c 100644 --- a/openbb_terminal/miscellaneous/models/hub_credentials.json +++ b/openbb_terminal/miscellaneous/models/hub_credentials.json @@ -35,6 +35,7 @@ "API_TWITTER_KEY": "", "API_TWITTER_SECRET_KEY": "", "API_TWITTER_BEARER_TOKEN": "", + "API_COMPANIESHOUSE_KEY": "", "API_DAPPRADAR_KEY": "", "API_KEY_NIXTLA": "" } diff --git a/openbb_terminal/sdk.py b/openbb_terminal/sdk.py index 6fb8c4260b55..7a0f8d24c2f1 100644 --- a/openbb_terminal/sdk.py +++ b/openbb_terminal/sdk.py @@ -95,6 +95,7 @@ def alt(self): `covid`: Covid Module `oss`: Oss Module `realestate`: Realestate Module + `companieshouse`: CompaniesHouse Module Attributes: `hn`: Get top stories from HackerNews.\n diff --git a/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_controller.py b/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_controller.py new file mode 100644 index 000000000000..0397aacd5615 --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_controller.py @@ -0,0 +1,172 @@ +"""Test the companieshouse controller.""" +import os + +import pytest + +from openbb_terminal.alternative.companieshouse import companieshouse_controller +from openbb_terminal.core.session.current_user import PreferencesModel, copy_user + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.record_stdout +def test_print_help(): + controller = companieshouse_controller.CompaniesHouseController(queue=None) + controller.print_help() + + +@pytest.mark.vcr(record_mode="none") +def test_menu_without_queue_completion(mocker): + preferences = PreferencesModel(USE_PROMPT_TOOLKIT=True) + mock_current_user = copy_user(preferences=preferences) + mocker.patch( + target="openbb_terminal.core.session.current_user.__current_user", + new=mock_current_user, + ) + mocker.patch( + target="openbb_terminal.parent_classes.session", + ) + mocker.patch( + target="openbb_terminal.parent_classes.session.prompt", + return_value="quit", + ) + mocker.patch( + target="openbb_terminal.alternative.companieshouse.companieshouse_controller.session", + ) + mocker.patch( + target="openbb_terminal.alternative.companieshouse.companieshouse_controller.session.prompt", + return_value="quit", + ) + + result_menu = companieshouse_controller.CompaniesHouseController().menu() + + assert result_menu == ["help"] + + +@pytest.mark.parametrize( + "mock_input", + ["help", "homee help", "home help", "mock"], +) +@pytest.mark.vcr(record_mode="none") +def test_menu_without_queue_sys_exit(mock_input, mocker): + path_controller = ( + "openbb_terminal.alternative.companieshouse.companieshouse_controller" + ) + + # DISABLE AUTO-COMPLETION + preferences = PreferencesModel( + USE_PROMPT_TOOLKIT=False, + ) + mock_current_user = copy_user(preferences=preferences) + mocker.patch( + target="openbb_terminal.core.session.current_user.__current_user", + new=mock_current_user, + ) + mocker.patch( + target=f"{path_controller}.session", + return_value=None, + ) + + # MOCK USER INPUT + mocker.patch("builtins.input", return_value=mock_input) + + # MOCK SWITCH + mocker.patch( + target=f"{path_controller}.CompaniesHouseController.switch", + return_value=["quit"], + ) + + # MOCK SWITCH + class SystemExitSideEffect: + def __init__(self): + self.first_call = True + + def __call__(self, *args, **kwargs): + if self.first_call: + self.first_call = False + raise SystemExit() + return ["quit"] + + mock_switch = mocker.Mock(side_effect=SystemExitSideEffect()) + + mocker.patch( + target=f"{path_controller}.CompaniesHouseController.switch", + new=mock_switch, + ) + + result_menu = companieshouse_controller.CompaniesHouseController().menu() + + assert result_menu == ["help"] + + +@pytest.mark.parametrize( + "an_input, expected_queue", + [ + ("", []), + ("/help", ["home", "help"]), + ("help/help", ["help", "help"]), + ("q", ["quit"]), + ("h", []), + ("home", ["quit", "quit"]), + ( + "r", + ["quit", "quit", "reset", "alternative", "companieshouse"], + ), + ], +) +def test_switch(an_input, expected_queue): + controller = companieshouse_controller.CompaniesHouseController(queue=None) + queue = controller.switch(an_input=an_input) + + assert queue == expected_queue + + +@pytest.mark.vcr(record_mode="none") +def test_call_cls(mocker): + mocker.patch("os.system") + + controller = companieshouse_controller.CompaniesHouseController(queue=None) + controller.call_cls([]) + + assert controller.queue == [] + + os.system.assert_called_once_with("cls||clear") + + +@pytest.mark.parametrize( + "func, queue, expected_queue", + [ + ( + "call_exit", + [], + ["quit", "quit", "quit"], + ), + ("call_exit", ["help"], ["quit", "quit", "quit", "help"]), + ("call_home", [], ["quit", "quit"]), + ("call_help", [], []), + ("call_quit", [], ["quit"]), + ("call_quit", ["help"], ["quit", "help"]), + ( + "call_reset", + [], + ["quit", "quit", "reset", "alternative", "companieshouse"], + ), + ( + "call_reset", + ["help"], + ["quit", "quit", "reset", "alternative", "companieshouse", "help"], + ), + ], +) +def test_call_func_expect_queue(expected_queue, func, queue): + controller = companieshouse_controller.CompaniesHouseController(queue=queue) + result = getattr(controller, func)([]) + + assert result is None + assert controller.queue == expected_queue + + +@pytest.mark.vcr(record_mode="none") +def test_quit(): + controller = companieshouse_controller.CompaniesHouseController(queue=None) + controller.call_quit([]) + assert controller.queue == ["quit"] diff --git a/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_model.py b/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_model.py new file mode 100644 index 000000000000..22ee7ee1ecc2 --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_model.py @@ -0,0 +1,299 @@ +import requests + +# IMPORTATION INTERNAL +from openbb_terminal.alternative.companieshouse import companieshouse_model +from openbb_terminal.alternative.companieshouse.company_doc import CompanyDocument + + +def test_get_search_results(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "total_results": 2369, + "items": [ + { + "kind": "searchresults#company", + "matches": {"title": [1, 5], "snippet": [13, 17]}, + "links": {"self": "/company/04366849"}, + "company_type": "plc", + "address": { + "premises": "Shell Centre", + "address_line_1": "London", + "postal_code": "SE1 7NA", + }, + "description": "04366849 - Incorporated on 5 February 2002", + "snippet": "ROYAL DUTCH SHELL ", + "title": "SHELL PLC", + "company_status": "active", + "date_of_creation": "2002-02-05", + "address_snippet": "Shell Centre, London, SE1 7NA", + "description_identifier": ["incorporated-on"], + "company_number": "04366849", + }, + { + "address_snippet": "Shell Centre, York Road, London, United Kingdom, SE1 7NA", + "description_identifier": ["incorporated-on"], + "company_number": "03323845", + "date_of_creation": "1997-02-25", + "links": {"self": "/company/03323845"}, + "matches": {"title": [1, 5], "snippet": [1, 5]}, + "company_type": "ltd", + "address": { + "address_line_1": "York Road", + "postal_code": "SE1 7NA", + "country": "United Kingdom", + "premises": "Shell Centre", + "locality": "London", + }, + "kind": "searchresults#company", + "snippet": "SHELL ", + "title": "SHELL GROUP LIMITED", + "company_status": "active", + "description": "03323845 - Incorporated on 25 February 1997", + }, + ], + "items_per_page": 2, + "kind": "search#companies", + "start_index": 0, + "page_number": 1, + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + result = companieshouse_model.get_search_results("shell", 2) + + assert len(result) == 2 + assert result["Name"].iloc[0] == "SHELL PLC" + assert result["Company Number"].iloc[0] == "04366849" + + +def test_get_persons_with_significant_control(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "ceased_count": 0, + "items_per_page": 25, + "total_results": 1, + "active_count": 1, + "start_index": 0, + "items": [ + { + "notified_on": "2020-12-18", + "links": { + "self": "/company/13090621/persons-with-significant-control/individual/vLLtcUU1xdVC4x3tXu2szkiDluE" + }, + "etag": "69ad137b49c648c6dd013761b82b52e6a7d474cb", + "kind": "individual-person-with-significant-control", + "country_of_residence": "England", + "name": "Mrs Shelley Ann Fray", + "natures_of_control": [ + "ownership-of-shares-75-to-100-percent", + "voting-rights-75-to-100-percent", + "right-to-appoint-and-remove-directors", + ], + "nationality": "British", + "address": { + "address_line_1": "Broadway", + "premises": "476-478", + "address_line_2": "Chadderton", + "country": "England", + "locality": "Oldham", + "postal_code": "OL9 9NS", + }, + "date_of_birth": {"month": 6, "year": 1985}, + "name_elements": { + "surname": "Fray", + "middle_name": "Ann", + "title": "Mrs", + "forename": "Shelley", + }, + } + ], + "links": {"self": "/company/13090621/persons-with-significant-control"}, + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + result = companieshouse_model.get_persons_with_significant_control("13090621") + + assert result["Name"].iloc[0] == "Mrs Shelley Ann Fray" + + +def test_get_officers(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "total_results": 1, + "links": {"self": "/company/13090621/officers"}, + "items_per_page": 35, + "items": [ + { + "links": { + "self": "/company/13090621/appointments/y2Xs68vEDqfz-zbmxzc4QucY8fQ", + "officer": { + "appointments": "/officers/MSVxvbR-XzoxCZazKccwdkkmCYE/appointments" + }, + }, + "date_of_birth": {"month": 6, "year": 1985}, + "address": { + "country": "England", + "address_line_2": "Chadderton", + "address_line_1": "Broadway", + "locality": "Oldham", + "premises": "476-478", + "postal_code": "OL9 9NS", + }, + "name": "FRAY, Shelley Ann", + "occupation": "Director", + "appointed_on": "2020-12-18", + "country_of_residence": "England", + "officer_role": "director", + "nationality": "British", + } + ], + "etag": "1138d965d225f6a0e5685c6d35e84e6a2dc0fc4f", + "resigned_count": 0, + "start_index": 0, + "inactive_count": 0, + "active_count": 1, + "kind": "officer-list", + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + result = companieshouse_model.get_officers("13090621") + + assert len(result) == 1 + assert result["Officer Role"].iloc[0] == "director" + + +def test_get_charges(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "etag": "0921370a2f5d8d191cc0c96de8de4ad7acdf68db", + "total_count": 1, + "unfiltered_count": 1, + "satisfied_count": 0, + "part_satisfied_count": 0, + "items": [ + { + "etag": "679e0c72b80543b2dc390b3d102163cf6cb89430", + "charge_code": "027235340001", + "classification": { + "type": "charge-description", + "description": "A registered charge", + }, + "charge_number": 1, + "status": "outstanding", + "delivered_on": "2014-12-04", + "created_on": "2014-11-26", + "particulars": { + "contains_fixed_charge": True, + "contains_negative_pledge": True, + }, + "persons_entitled": [{"name": "Astrazeneca Pensions Trustee Limited "}], + "transactions": [ + { + "filing_type": "create-charge-with-deed", + "delivered_on": "2014-12-04", + "links": { + "filing": "/company/02723534/filing-history/MzExMzExMzg3N2FkaXF6a2N4" + }, + } + ], + "links": { + "self": "/company/02723534/charges/ObJFuAILDHfp00ro3wjqXEonL7k" + }, + } + ], + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + result = companieshouse_model.get_charges("13090621") + + assert len(result) == 1 + + +def test_get_filings(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "total_count": 3, + "items_per_page": 25, + "items": [ + { + "action_date": "2022-12-17", + "category": "confirmation-statement", + "date": "2023-01-06", + "description": "confirmation-statement-with-no-updates", + "description_values": {"made_up_date": "2022-12-17"}, + "links": { + "self": "/company/13090621/filing-history/MzM2NDcxNTAzMWFkaXF6a2N4", + "document_metadata": "https://frontend-doc-api.company-information.service.gov.uk/document/ZIvX", + }, + "type": "CS01", + "pages": 3, + "barcode": "XBUK9D4X", + "transaction_id": "MzM2NDcxNTAzMWFkaXF6a2N4", + }, + { + "category": "accounts", + "date": "2022-09-14", + "description": "accounts-with-accounts-type-unaudited-abridged", + "description_values": {"made_up_date": "2021-12-31"}, + "links": { + "self": "/company/13090621/filing-history/MzM1MTY4NjU5NmFkaXF6a2N4", + "document_metadata": "https://frontend-doc-api.company-information.service.gov.uk/document/8_jg", + }, + "type": "AA", + "pages": 9, + "barcode": "XBCG6V3N", + "transaction_id": "MzM1MTY4NjU5NmFkaXF6a2N4", + }, + { + "action_date": "2022-09-09", + "category": "address", + "date": "2022-09-09", + "description": "change-registered-office-address-company-with-date-old-address-new-address", + "description_values": { + "change_date": "2022-09-09", + "old_address": "1J Shillington Old School Este Road London SW11 2TB", + "new_address": "476-478 Broadway Chadderton Oldham OL9 9NS", + }, + "links": { + "self": "/company/13090621/filing-history/MzM1MTIwMjE4NGFkaXF6a2N4", + "document_metadata": "https://frontend-doc-api.company-information.service.gov.uk/document/h1b5B84v", + }, + "type": "AD01", + "pages": 1, + "barcode": "XBC30YEG", + "transaction_id": "MzM1MTIwMjE4NGFkaXF6a2N4", + }, + ], + "start_index": 0, + "filing_history_status": "filing-history-available", + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + result = companieshouse_model.get_filings("13090621", 0) + + assert len(result.filings) == 3 + assert result.filings["Category"].iloc[0] == "confirmation-statement" + + +def test_get_filing_document(mocker): + result = CompanyDocument( + "confirmation-statement", + "2023-01-06", + "confirmation-statement-with-no-updates", + "paper_filed", + 3, + "MzM2NDcxNTAzMWFkaXF6a2N4", + "content", + ) + + assert result.description == "confirmation-statement-with-no-updates" + assert result.pages == 3 diff --git a/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_view.py b/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_view.py new file mode 100644 index 000000000000..dede9e7512dc --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/test_companieshouse_view.py @@ -0,0 +1,283 @@ +import pandas as pd +import pytest +import requests + +# IMPORTATION INTERNAL +from openbb_terminal.alternative.companieshouse import companieshouse_view +from openbb_terminal.helper_funcs import print_rich_table + + +@pytest.mark.vcr +@pytest.mark.record_stdout +def test_display_search(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "total_results": 2369, + "items": [ + { + "kind": "searchresults#company", + "matches": {"title": [1, 5], "snippet": [13, 17]}, + "links": {"self": "/company/04366849"}, + "company_type": "plc", + "address": { + "premises": "Shell Centre", + "address_line_1": "London", + "postal_code": "SE1 7NA", + }, + "description": "04366849 - Incorporated on 5 February 2002", + "snippet": "ROYAL DUTCH SHELL ", + "title": "SHELL PLC", + "company_status": "active", + "date_of_creation": "2002-02-05", + "address_snippet": "Shell Centre, London, SE1 7NA", + "description_identifier": ["incorporated-on"], + "company_number": "04366849", + }, + { + "address_snippet": "Shell Centre, York Road, London, United Kingdom, SE1 7NA", + "description_identifier": ["incorporated-on"], + "company_number": "03323845", + "date_of_creation": "1997-02-25", + "links": {"self": "/company/03323845"}, + "matches": {"title": [1, 5], "snippet": [1, 5]}, + "company_type": "ltd", + "address": { + "address_line_1": "York Road", + "postal_code": "SE1 7NA", + "country": "United Kingdom", + "premises": "Shell Centre", + "locality": "London", + }, + "kind": "searchresults#company", + "snippet": "SHELL ", + "title": "SHELL GROUP LIMITED", + "company_status": "active", + "description": "03323845 - Incorporated on 25 February 1997", + }, + ], + "items_per_page": 2, + "kind": "search#companies", + "start_index": 0, + "page_number": 1, + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + companieshouse_view.display_search("shell", 2) + + +@pytest.mark.vcr +@pytest.mark.record_stdout +def test_display_persons_with_significant_control(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "ceased_count": 0, + "items_per_page": 25, + "total_results": 1, + "active_count": 1, + "start_index": 0, + "items": [ + { + "notified_on": "2020-12-18", + "links": { + "self": "/company/13090621/persons-with-significant-control/individual/vLLtcUU1xdVC4x3tXu2szkiDluE" + }, + "etag": "69ad137b49c648c6dd013761b82b52e6a7d474cb", + "kind": "individual-person-with-significant-control", + "country_of_residence": "England", + "name": "Mrs Shelley Ann Fray", + "natures_of_control": [ + "ownership-of-shares-75-to-100-percent", + "voting-rights-75-to-100-percent", + "right-to-appoint-and-remove-directors", + ], + "nationality": "British", + "address": { + "address_line_1": "Broadway", + "premises": "476-478", + "address_line_2": "Chadderton", + "country": "England", + "locality": "Oldham", + "postal_code": "OL9 9NS", + }, + "date_of_birth": {"month": 6, "year": 1985}, + "name_elements": { + "surname": "Fray", + "middle_name": "Ann", + "title": "Mrs", + "forename": "Shelley", + }, + } + ], + "links": {"self": "/company/13090621/persons-with-significant-control"}, + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + companieshouse_view.display_persons_with_significant_control("13090621") + + +@pytest.mark.vcr +@pytest.mark.record_stdout +def test_display_officers(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "total_results": 1, + "links": {"self": "/company/13090621/officers"}, + "items_per_page": 35, + "items": [ + { + "links": { + "self": "/company/13090621/appointments/y2Xs68vEDqfz-zbmxzc4QucY8fQ", + "officer": { + "appointments": "/officers/MSVxvbR-XzoxCZazKccwdkkmCYE/appointments" + }, + }, + "date_of_birth": {"month": 6, "year": 1985}, + "address": { + "country": "England", + "address_line_2": "Chadderton", + "address_line_1": "Broadway", + "locality": "Oldham", + "premises": "476-478", + "postal_code": "OL9 9NS", + }, + "name": "FRAY, Shelley Ann", + "occupation": "Director", + "appointed_on": "2020-12-18", + "country_of_residence": "England", + "officer_role": "director", + "nationality": "British", + } + ], + "etag": "1138d965d225f6a0e5685c6d35e84e6a2dc0fc4f", + "resigned_count": 0, + "start_index": 0, + "inactive_count": 0, + "active_count": 1, + "kind": "officer-list", + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + companieshouse_view.display_officers("13090621") + + +@pytest.mark.vcr +@pytest.mark.record_stdout +def test_display_charges(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "etag": "0921370a2f5d8d191cc0c96de8de4ad7acdf68db", + "total_count": 1, + "unfiltered_count": 1, + "satisfied_count": 0, + "part_satisfied_count": 0, + "items": [ + { + "etag": "679e0c72b80543b2dc390b3d102163cf6cb89430", + "charge_code": "027235340001", + "classification": { + "type": "charge-description", + "description": "A registered charge", + }, + "charge_number": 1, + "status": "outstanding", + "delivered_on": "2014-12-04", + "created_on": "2014-11-26", + "particulars": { + "contains_fixed_charge": True, + "contains_negative_pledge": True, + }, + "persons_entitled": [{"name": "Astrazeneca Pensions Trustee Limited "}], + "transactions": [ + { + "filing_type": "create-charge-with-deed", + "delivered_on": "2014-12-04", + "links": { + "filing": "/company/02723534/filing-history/MzExMzExMzg3N2FkaXF6a2N4" + }, + } + ], + "links": { + "self": "/company/02723534/charges/ObJFuAILDHfp00ro3wjqXEonL7k" + }, + } + ], + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + df = pd.DataFrame.from_records(mock_response.json.return_value["items"]).iloc[0] + # Call the function that makes a request to the remote API + print_rich_table(df, show_index=True, title="Charges") + + +@pytest.mark.vcr +@pytest.mark.record_stdout +def test_display_filings(mocker): + mock_response = mocker.Mock() + mock_response.json.return_value = { + "total_count": 3, + "items_per_page": 25, + "items": [ + { + "action_date": "2022-12-17", + "category": "confirmation-statement", + "date": "2023-01-06", + "description": "confirmation-statement-with-no-updates", + "description_values": {"made_up_date": "2022-12-17"}, + "links": { + "self": "/company/13090621/filing-history/MzM2NDcxNTAzMWFkaXF6a2N4", + "document_metadata": "https://frontend-doc-api.company-information.service.gov.uk/document/ZIvX", + }, + "type": "CS01", + "pages": 3, + "barcode": "XBUK9D4X", + "transaction_id": "MzM2NDcxNTAzMWFkaXF6a2N4", + }, + { + "category": "accounts", + "date": "2022-09-14", + "description": "accounts-with-accounts-type-unaudited-abridged", + "description_values": {"made_up_date": "2021-12-31"}, + "links": { + "self": "/company/13090621/filing-history/MzM1MTY4NjU5NmFkaXF6a2N4", + "document_metadata": "https://frontend-doc-api.company-information.service.gov.uk/document/8_jg", + }, + "type": "AA", + "pages": 9, + "barcode": "XBCG6V3N", + "transaction_id": "MzM1MTY4NjU5NmFkaXF6a2N4", + }, + { + "action_date": "2022-09-09", + "category": "address", + "date": "2022-09-09", + "description": "change-registered-office-address-company-with-date-old-address-new-address", + "description_values": { + "change_date": "2022-09-09", + "old_address": "1J Shillington Old School Este Road London SW11 2TB", + "new_address": "476-478 Broadway Chadderton Oldham OL9 9NS", + }, + "links": { + "self": "/company/13090621/filing-history/MzM1MTIwMjE4NGFkaXF6a2N4", + "document_metadata": "https://frontend-doc-api.company-information.service.gov.uk/document/h1b5B84v", + }, + "type": "AD01", + "pages": 1, + "barcode": "XBC30YEG", + "transaction_id": "MzM1MTIwMjE4NGFkaXF6a2N4", + }, + ], + "start_index": 0, + "filing_history_status": "filing-history-available", + } + + mocker.patch.object(requests, "get", return_value=mock_response) + + # Call the function that makes a request to the remote API + companieshouse_view.display_filings("13090621", 0) diff --git a/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_controller/test_print_help.txt b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_controller/test_print_help.txt new file mode 100644 index 000000000000..ef2478d34143 --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_controller/test_print_help.txt @@ -0,0 +1,10 @@ +Current Company: + + search search for companies by name + load retrieve basic company details by company registration number + officers retrieve company officies + signifcontrol retrieve those with significant control over the company + filings retrieve list of documents filed with Companies House (Default 100) + filingdocument download filed document + charges registered charges against the company + diff --git a/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_charges.txt b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_charges.txt new file mode 100644 index 000000000000..45ea7729494e --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_charges.txt @@ -0,0 +1,11 @@ +etag 679e0c72b80543b2dc390b3d102163cf6cb89430 +charge_code 027235340001 +classification {'type': 'charge-description', 'description': ... +charge_number 1 +status outstanding +delivered_on 2014-12-04 +created_on 2014-11-26 +particulars {'contains_fixed_charge': True, 'contains_nega... +persons_entitled [{'name': 'Astrazeneca Pensions Trustee Limite... +transactions [{'filing_type': 'create-charge-with-deed', 'd... +links {'self': '/company/02723534/charges/ObJFuAILDH... diff --git a/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_filings.txt b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_filings.txt new file mode 100644 index 000000000000..02d7bf5b6881 --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_filings.txt @@ -0,0 +1,5 @@ +Retrieved 3 filings + Category Date Description Type Pages Transaction ID Download +0 confirmation-statement 2023-01-06 confirmation-statement-with-no-updates CS01 3 MzM2NDcxNTAzMWFkaXF6a2N4 https://find-and-update.company-information.service.gov.uk/company/13090621/filing-history/MzM2NDcxNTAzMWFkaXF6a2N4/document?format=pdf&download=0 +1 accounts 2022-09-14 accounts-with-accounts-type-unaudited-abridged AA 9 MzM1MTY4NjU5NmFkaXF6a2N4 https://find-and-update.company-information.service.gov.uk/company/13090621/filing-history/MzM1MTY4NjU5NmFkaXF6a2N4/document?format=pdf&download=0 +2 address 2022-09-09 change-registered-office-address-company-with-date-old-address-new-address AD01 1 MzM1MTIwMjE4NGFkaXF6a2N4 https://find-and-update.company-information.service.gov.uk/company/13090621/filing-history/MzM1MTIwMjE4NGFkaXF6a2N4/document?format=pdf&download=0 diff --git a/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_officers.txt b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_officers.txt new file mode 100644 index 000000000000..8e52e5cfbcf0 --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_officers.txt @@ -0,0 +1,3 @@ +Retrieved 1 records + Officer Role Appointed On Resigned On Name +0 director 2020-12-18 - FRAY, Shelley Ann diff --git a/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_persons_with_significant_control.txt b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_persons_with_significant_control.txt new file mode 100644 index 000000000000..768bee61d87e --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_persons_with_significant_control.txt @@ -0,0 +1,3 @@ +Retrieved 1 records + Kind Name Natures of Control Notified On +0 individual-person-with-significant-control Mrs Shelley Ann Fray [ownership-of-shares-75-to-100-percent, voting-rights-75-to-100-percent, right-to-appoint-and-remove-directors] 2020-12-18 diff --git a/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_search.txt b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_search.txt new file mode 100644 index 000000000000..5e1e69257de7 --- /dev/null +++ b/tests/openbb_terminal/alternative/companieshouse/txt/test_companieshouse_view/test_display_search.txt @@ -0,0 +1,4 @@ +Retrieved 2 records + Name Company Number Status +0 SHELL PLC 04366849 active +1 SHELL GROUP LIMITED 03323845 active diff --git a/tests/openbb_terminal/alternative/txt/test_alt_controller/test_print_help.txt b/tests/openbb_terminal/alternative/txt/test_alt_controller/test_print_help.txt index 801cfd9834b5..00374ecbce24 100644 --- a/tests/openbb_terminal/alternative/txt/test_alt_controller/test_print_help.txt +++ b/tests/openbb_terminal/alternative/txt/test_alt_controller/test_print_help.txt @@ -1,6 +1,7 @@ -> covid COVID menu cases, deaths, rates -> oss Open Source menu star history, repos information -> realestate Real Estate menu sold prices, HPIs +> covid COVID menu, cases, deaths, rates +> oss Open Source menu, star history, repos information +> realestate Real Estate menu, sold prices, HPIs +> companieshouse Companies House menu, UK company information hn Hacker News most popular stories [HackerNews] diff --git a/website/content/sdk/usage/guides/api-keys.md b/website/content/sdk/usage/guides/api-keys.md index 3d751db48438..288b26b14086 100644 --- a/website/content/sdk/usage/guides/api-keys.md +++ b/website/content/sdk/usage/guides/api-keys.md @@ -4,10 +4,9 @@ sidebar_position: 1 description: API (Application Programming Interface) keys are access credentials for accessing data from a particular source. Learn how to set, manage, and access data APIs for the OpenBB SDK. keywords: [api, keys, api keys, data provider, data, free, alpha vantage, fred, iex, twitter, degiro, binance, coinglass, polygon, intrinio, sdk, alphavantage, bitquery, coinbase, databento, finnhub, FRED, github, glassnode, iex cloud, news API, robinhood, santiment, shroomdk, token terminal, tradier, twitter, whale alert] --- - import HeadTitle from '@site/src/components/General/HeadTitle.tsx'; - +`` ## The Keys Module @@ -176,8 +175,6 @@ openbb.keys.biztoc(key = "REPLACE_WITH_KEY", persist=True) ### CoinMarketCap -### CoinMarketCap - > CoinMarketCap is the world's most-referenced price-tracking website for cryptoassets in the rapidly growing cryptocurrency space. Its mission is to make crypto discoverable and efficient globally by empowering retail users with unbiased, high quality and accurate information for drawing their own informed conclusions.
@@ -250,6 +247,29 @@ openbb.keys.coinglass(key = 'REPLACE_WITH_KEY', persist = True)
+### Companies House + +> The Companies House API lets you retrieve information about limited companies (and other companies that fall within the Companies Act 2006). The data returned is live and real-time, and is simple to use and understand. + +
+Instructions + +Setup an account by following the instructions [here](https://developer-specs.company-information.service.gov.uk/guides/gettingStarted#set-up-a-companies-house-account). + +![Companies House](https://user-images.githubusercontent.com/85772166/234980988-352e36f7-334b-4e4c-846f-2cea128f9464.png) + +After creating the account and logging in, click on, [Create an Application](https://developer.company-information.service.gov.uk/manage-applications/add). Fill out the form and then click the `Create` button at the bottom of the page. + +![Create an Application](https://user-images.githubusercontent.com/85772166/234981083-e78cf4f7-3be0-404e-b7cc-b452679bbb06.png) + +Once the application is created, an API key can be generated by clicking on `View all Applications`, and then the `Create new key` button at the bottom. Copy the API key value to the clipboard and then enter it into the OpenBB SDK with: + +```console +openbb.keys.companieshouse('REPLACE_WITH_KEY', persist = True) +``` + +
+ ### Crypto Panic > CryptoPanic is a news aggregator platform indicating impact on price and market for traders and cryptocurrency enthusiasts. diff --git a/website/content/terminal/usage/guides/api-keys.md b/website/content/terminal/usage/guides/api-keys.md index 75ff8caac824..46a3601b90ac 100644 --- a/website/content/terminal/usage/guides/api-keys.md +++ b/website/content/terminal/usage/guides/api-keys.md @@ -223,6 +223,29 @@ With the account created, find the assigned API key within the account profile p +### Companies House + +> The Companies House API lets you retrieve information about limited companies (and other companies that fall within the Companies Act 2006). The data returned is live and real-time, and is simple to use and understand. + +
+Instructions + +Setup an account by following the instructions [here](https://developer-specs.company-information.service.gov.uk/guides/gettingStarted#set-up-a-companies-house-account). + +![Companies House](https://user-images.githubusercontent.com/85772166/234980988-352e36f7-334b-4e4c-846f-2cea128f9464.png) + +After creating the account and logging in, click on, [Create an Application](https://developer.company-information.service.gov.uk/manage-applications/add). Fill out the form and then click the `Create` button at the bottom of the page. + +![Create an Application](https://user-images.githubusercontent.com/85772166/234981083-e78cf4f7-3be0-404e-b7cc-b452679bbb06.png) + +Once the application is created, an API key can be generated by clicking on `View all Applications`, and then the `Create new key` button at the bottom. Copy the API key value to the clipboard and then enter it into the OpenBB Terminal with: + +```console +keys/companieshouse -k REPLACE_WITH_KEY +``` + +
+ ### Crypto Panic > CryptoPanic is a news aggregator platform indicating impact on price and market for traders and cryptocurrency enthusiasts.