From d4a044431f4edb2d99fbaf034dbf35e12fdf5444 Mon Sep 17 00:00:00 2001 From: Sandeep Murthy Date: Sat, 13 Sep 2025 14:35:52 +0100 Subject: [PATCH 1/3] chore: bump version refs to `v1.1.0` --- CONTRIBUTING.md | 2 +- README.md | 2 +- docs/index.rst | 2 +- docs/sources/contributing.rst | 2 +- src/financial_services_register_api/__version__.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb3f2ce..116c59f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,7 +130,7 @@ and the [CodeQL Analysis YML](https://github.com/sr-murthy/fsrapiclient/blob/mai ## Versioning and Releases -The [PyPI package](https://pypi.org/project/fsrapiclient/) is currently at version `1.0.0`. +The [PyPI package](https://pypi.org/project/fsrapiclient/) is currently at version `1.1.0`. There is currently no dedicated pipeline for releases - both [GitHub releases](https://github.com/sr-murthy/fsrapiclient/releases) and [PyPI packages](https://pypi.org/project/fsrapiclient) are published manually, but both have the same version tag. diff --git a/README.md b/README.md index 6889379..360948d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A lightweight Python client library for the UK [Financial Services Register](https://register.fca.org.uk/s/) [RESTful API](https://register.fca.org.uk/Developer/s/). -The [PyPI package](https://pypi.org/project/financial-services-register-api) is currently at version `1.0.0`. +The [PyPI package](https://pypi.org/project/financial-services-register-api) is currently at version `1.1.0`. > [!NOTE] > The new package `financial-services-register-api` supersedes the older package `fsrapiclient`, which will no longer be published. Existing versions of the older package may be retracted in the future. Please use the new package. diff --git a/docs/index.rst b/docs/index.rst index 0f217fe..f09a20b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ financial-services-register-api A Python client library for the UK `Financial Services Register `_ `RESTful API `_. -The `PyPI package `_ is currently at version `1.0.0`. +The `PyPI package `_ is currently at version `1.1.0`. .. note:: diff --git a/docs/sources/contributing.rst b/docs/sources/contributing.rst index 8d771a2..ca6e788 100644 --- a/docs/sources/contributing.rst +++ b/docs/sources/contributing.rst @@ -158,7 +158,7 @@ and the `CodeQL Analysis YML `_ is currently at version ``1.0.0``. +The `PyPI package `_ is currently at version ``1.1.0``. There is currently no dedicated pipeline for releases - both `GitHub releases `_ and `PyPI packages `_ are published manually, but both have the same version tag. diff --git a/src/financial_services_register_api/__version__.py b/src/financial_services_register_api/__version__.py index 5becc17..6849410 100644 --- a/src/financial_services_register_api/__version__.py +++ b/src/financial_services_register_api/__version__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.1.0" From 51dadc5be89da8f625d4c25fcc3fdd0dbf4cc3de Mon Sep 17 00:00:00 2001 From: Sandeep Murthy Date: Sat, 13 Sep 2025 18:24:31 +0100 Subject: [PATCH 2/3] All changes for `v1.1.0` --- docs/conf.py | 31 +-- docs/index.rst | 2 +- .../financial-services-register-api.rst | 6 +- .../financial_services_register_api/api.rst | 2 +- docs/sources/usage.rst | 61 +++-- src/financial_services_register_api/api.py | 222 ++++++++---------- .../constants.py | 1 + tests/units/test_api.py | 84 ++++--- 8 files changed, 195 insertions(+), 214 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 894c255..77d6ae3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,9 +76,8 @@ # Publish author(s) show_authors = True -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. +# Sphinx extensions: not all of these are used or required, but they are still +# listed here if requirements change. extensions = ['jupyter_sphinx', 'matplotlib.sphinxext.plot_directive', 'myst_parser', @@ -106,10 +105,10 @@ # For more on all available autodoc defaults see # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_default_options autodoc_default_options = { - 'exclude-members': '__call__,__weakref__,__slots__,__match_args__', + 'exclude-members': '', 'member-order': 'bysource', - 'private-members': True, - 'special-members': '__eq__, __init__, __new__' + 'private-members': False, + 'special-members': '__init__,__new__' } # Sphinx autodoc autosummary settings @@ -122,24 +121,10 @@ numpydoc_attributes_as_param_list = False numpydoc_xref_param_type = False -# Intersphinx mappings to reference external documentation domains -intersphinx_mapping = { - 'coverage': ('https://coverage.readthedocs.io/en/7.3.1/', None), - 'matplotlib': ('https://matplotlib.org/stable/', None), - 'networkx': ('https://networkx.org/documentation/stable/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), - 'pdm': ('https://pdm-project.org/latest/', None), - 'pygraphviz': ('https://pygraphviz.github.io/documentation/stable/', None), - 'pytest': ('https://docs.pytest.org/en/7.4.x/', None), - 'python': ('https://docs.python.org/3', None), - 'requests': ('https://requests.readthedocs.io/en/latest/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/', None), - 'sympy': ('https://docs.sympy.org/latest/', None), - 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), -} +# Intersphinx mappings to reference external documentation domains - none required. +intersphinx_mapping = {} -# Add any paths that contain templates here, relative to this directory. +# Static template paths templates_path = ['_templates'] # The suffix of source filenames. diff --git a/docs/index.rst b/docs/index.rst index f09a20b..c14c796 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ The `PyPI package `_ i The new package `financial-services-register-api` supersedes the older package `fsrapiclient`, which will no longer be published. Existing versions of the older package may be retracted in the future. Please use the new package. -The Financial Services Register (alternatively, FS Register), is a **public** database of all firms, individuals, funds, and other entities, that are either currently, or have been previously, authorised and/or regulated by the UK `Financial Conduct Authority (FCA) `_ and/or the `Prudential Regulation Authority (PRA) `_. +The Financial Services Register (or simply, the Register), is a **public** database of all firms, individuals, funds, and other entities, that are either currently, or have been previously, authorised and/or regulated by the UK `Financial Conduct Authority (FCA) `_ and/or the `Prudential Regulation Authority (PRA) `_. .. note:: diff --git a/docs/sources/financial-services-register-api.rst b/docs/sources/financial-services-register-api.rst index 460f994..7710c0d 100644 --- a/docs/sources/financial-services-register-api.rst +++ b/docs/sources/financial-services-register-api.rst @@ -56,7 +56,7 @@ According to the `API documentation `_ Firm Requests ============= -Firms in the FS Register are identified by unique firm reference numbers (FRN). The following table summarises firm-specific API endpoints. For further details consult the `API documentation `_. +Firms in the Register are identified by unique firm reference numbers (FRN). The following table summarises firm-specific API endpoints. For further details consult the `API documentation `_. .. list-table:: :align: left @@ -121,7 +121,7 @@ For details and examples on calling these endpoints via this library see :ref:`t Individual Requests =================== -Individuals associated with firms in the FS Register are identified by unique individual reference numbers (IRN). The following table summarises individual-specific API endpoints. +Individuals associated with firms in the Register are identified by unique individual reference numbers (IRN). The following table summarises individual-specific API endpoints. .. list-table:: :align: left @@ -152,7 +152,7 @@ For how to call these endpoints see :ref:`this `. Fund Requests ============= -Funds, also referred to as collective investment schemes (CIS) in the FS Register, are identified by unique product reference numbers (PRN). The following table summarises fund-specific API endpoints. +Funds, also referred to as collective investment schemes (CIS) in the Register, are identified by unique product reference numbers (PRN). The following table summarises fund-specific API endpoints. .. list-table:: :align: left diff --git a/docs/sources/financial_services_register_api/api.rst b/docs/sources/financial_services_register_api/api.rst index 1bc1935..d54c754 100644 --- a/docs/sources/financial_services_register_api/api.rst +++ b/docs/sources/financial_services_register_api/api.rst @@ -8,4 +8,4 @@ .. automodule:: financial_services_register_api.api :members: - :private-members: + :special-members: diff --git a/docs/sources/usage.rst b/docs/sources/usage.rst index 13cdc50..f9148e3 100644 --- a/docs/sources/usage.rst +++ b/docs/sources/usage.rst @@ -33,11 +33,11 @@ storing the API username (signup email) and API key. These, and also the API ver >>> client.api_version 'V0.1' -Almost all public client methods return :py:class:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse` objects, which have four properties specific to the FS Register API: +Almost all public client methods return :py:class:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse` objects, which have four properties specific to the API: -- :py:attr:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse.status` - an FS Register-specific status indicator for the +- :py:attr:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse.status` - an API-specific status indicator for the request -- :py:attr:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse.message` - an FS Register-specific status message for the +- :py:attr:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse.message` - an API-specific status message for the request - :py:attr:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse.data` - the response data - :py:attr:`~financial_services_register_api.api.FinancialServicesRegisterApiResponse.resultinfo` - pagination information for the response data @@ -172,48 +172,41 @@ The client implements a `regulated markets >> client.search_frn('hiscox insurance company limited') '113849' -Imprecise names in the search can produce multiple records, and will trigger an :py:class:`~financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException` indicating the problem, e.g.: +Imprecise or inadequality specified names in the search can produce non-unique matches, in which all matching records are returned in a JSON array, for example: .. code:: python >>> client.search_frn('hiscox') - Traceback (most recent call last): - ... - financial_services_register_api.api.FinancialServicesRegisterApiResponseException: Multiple firms returned. Firm name needs to be more precise. If you are unsure of the results please use the common search endpoint - -In this case the exception was generated because a common search for ``'hiscox'`` shows that there are multiple firms entries containing this name fragment: - -.. code:: python - - >>> client.common_search(urlencode({'q': 'hiscox', 'type': 'firm'})).data [{'URL': 'https://register.fca.org.uk/services/V0.1/Firm/812274', 'Status': 'No longer authorised', 'Reference Number': '812274', 'Type of business or Individual': 'Firm', 'Name': 'HISCOX ASSURE'}, - ... + ... + ... {'URL': 'https://register.fca.org.uk/services/V0.1/Firm/732312', 'Status': 'Authorised', 'Reference Number': '732312', 'Type of business or Individual': 'Firm', - 'Name': 'Hiscox MGA Ltd (Postcode: EC2N 4BQ)'}] + 'Name': 'Hiscox MGA Ltd (Postcode: EC2N 4BQ)'} + ] -Searches for non-existent firms will trigger an :py:class:`~financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException` indicating that no data found in the FS Register for the given name: +Searches for non-existent firms will trigger an :py:class:`~financial_services_register_api.exceptions.FinancialServicesRegisterApiRequestException` indicating that no data found in the Register for the given name: .. code:: python @@ -230,9 +223,18 @@ A few examples are given below of IRN searches. 'MXC29012' # >>> client.search_irn('mark c') - Traceback (most recent call last): - ... - financial_services_register_api.api.FinancialServicesRegisterApiResponseException: Multiple individuals returned. The individual name needs to be more precise. If you are unsure of the results please use the common search endpoint + [{'URL': 'https://register.fca.org.uk/services/V0.1/Individuals/MWC01033', + 'Status': 'Active', + 'Reference Number': 'MWC01033', + 'Type of business or Individual': 'Individual', + 'Name': 'Mark William Cowell'}, + ... + ... + {'URL': 'https://register.fca.org.uk/services/V0.1/Individuals/RMG01106', + 'Status': 'Active', + 'Reference Number': 'RMG01106', + 'Type of business or Individual': 'Individual', + 'Name': 'Richard Mark Greenfield'}] # >>> client.search_irn('a nonexistent individual') Traceback (most recent call last): @@ -247,9 +249,18 @@ A few examples are given below of PRN searches. '635641' # >>> client.search_prn('jupiter asia') - Traceback (most recent call last): - ... - financial_services_register_api.api.FinancialServicesRegisterApiResponseException: Multiple funds returned. The fund name needs to be more precise. If you are unsure of the results please use the common search endpoint + [{'URL': 'https://register.fca.org.uk/services/V0.1/CIS/718428', + 'Status': 'Authorised', + 'Reference Number': '718428', + 'Type of business or Individual': 'Collective investment scheme', + 'Name': 'Jupiter Asian Income Fund'}, + ... + ... + {'URL': 'https://register.fca.org.uk/services/V0.1/CIS/140620', + 'Status': 'Terminated', + 'Reference Number': '140620', + 'Type of business or Individual': 'Collective investment scheme', + 'Name': 'JUPITER ASIAN FUND'}] # >>> client.search_prn('a nonexistent fund') Traceback (most recent call last): diff --git a/src/financial_services_register_api/api.py b/src/financial_services_register_api/api.py index 02e466c..3aa2e3d 100644 --- a/src/financial_services_register_api/api.py +++ b/src/financial_services_register_api/api.py @@ -201,10 +201,9 @@ class FinancialServicesRegisterApiClient: >>> assert res.resultinfo >>> client.search_frn("Hastings Insurance Services Limited") '311492' - >>> client.search_frn('direct line') - Traceback (most recent call last): - ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: Multiple firms returned. The firm name needs to be more precise. If you are unsure of the results please use the common search endpoint. + >>> res = client.search_frn('direct line') + >>> assert isinstance(res, list) + >>> assert (isinstance(rec, dict) for rec in res) >>> client.search_frn('direct line insurance plc') '202684' >>> assert client.get_firm('122702').data @@ -334,8 +333,8 @@ def common_search(self, resource_name: str, resource_type: Literal['firm', 'indi except requests.RequestException as e: raise FinancialServicesRegisterApiRequestException(e) - def _search_ref_number(self, resource_name: str, resource_type: str) -> str: - """:py:class:`str`: A private base handler for public methods for searching for unique firm, individual and product reference numbers. + def _search_ref_number(self, resource_name: str, resource_type: str, /) -> str | list[dict[str, str]]: + """:py:class:`str` or :py:class:`list`: A private base handler for public search methods for unique firm, individual and product reference numbers. .. note:: @@ -348,11 +347,14 @@ def _search_ref_number(self, resource_name: str, resource_type: str) -> str: /V0.1/Search?q=resource_name&type=resource_type to perform a case-insensitive search for resources of type - ``resource_type`` in the FS Register on the given resource name - substring. + ``resource_type`` in the Financial Services Register on the given + resource name substring. Returns a non-null string of the resource ref. number if there is - a unique associated resource. + a unique associated resource. Otherwise returns :py:class. + + If there are multiple resources matching the given resource name + substring then a JSON array of the matching records is returned. Parameters @@ -362,21 +364,26 @@ def _search_ref_number(self, resource_name: str, resource_type: str) -> str: The name needs to be precise enough to guarantee a unique return value, otherwise multiple records exist and an exception is raised. + resource_type : str + The resource type, which should be one of ``'firm'``, + ``'individual'``, or ``'fund'``. + + Returns + ------- + str, list + The unique resource reference number, if found. Otherwise + a JSON array of matching records. + Raises ------ ValueError If the resource type is not of ``'firm'``, ``'individual'``, or ``'fund'``. FinancialServicesRegisterApiRequestException - If there was a request exception from calling the common search - handler. - FinancialServicesRegisterApiException - If there was an error in the API response or in processing the response. - - Returns - ------- - str - The unique resource reference number, if found. + If there was an API request exception. + FinancialServicesRegisterApiResponseException + If the API response does not conform to the expected structure, or + no data was found for the given resource type and name. """ if resource_type not in API_CONSTANTS.RESOURCE_TYPES.value: raise ValueError( @@ -390,54 +397,51 @@ def _search_ref_number(self, resource_name: str, resource_type: str) -> str: raise if res.ok and res.data: + if len(res.data) == 1: + try: + return res.data[0]['Reference Number'] + except KeyError: + raise FinancialServicesRegisterApiResponseException( + 'Unexpected response data structure from the API for ' + f'{resource_type} search by name "{resource_name}"! ' + 'Please check the API developer documentation at ' + f'{API_CONSTANTS.DEVELOPER_PORTAL.value}.' + ) if len(res.data) > 1: - raise FinancialServicesRegisterApiResponseException( - f'Multiple {resource_type}s returned. The {resource_type} ' - 'name needs to be more precise. If you are unsure of the ' - 'results please use the common search endpoint.' - ) - - try: - return res.data[0]['Reference Number'] - except (KeyError, IndexError): - raise FinancialServicesRegisterApiResponseException( - 'Unexpected response data structure from the FS Register ' - f'API for general {resource_type} search by name! Please ' - 'check the FS Register API developer documentation at ' - 'https://register.fca.org.uk/Developer/s/.' - ) + return res.data + elif not res.ok: + raise FinancialServicesRegisterApiRequestException( + f'API search request failed for an unknown reason: ' + f'{res.reason}. Please check the search parameters and try again.' + ) elif not res.data: - raise FinancialServicesRegisterApiResponseException( - 'No data found in FS Register API response. Please check the search ' + raise FinancialServicesRegisterApiRequestException( + 'No data found in the API response. Please check the search ' 'parameters and try again.' ) - else: - raise FinancialServicesRegisterApiResponseException( - f'FS Register API search request failed for some other reason: ' - f'{res.reason}.' - ) - def search_frn(self, firm_name: str) -> str: - """:py:class:`str`: Returns the unique firm reference number (FRN) of a given firm, if found. + def search_frn(self, firm_name: str) -> str | list[dict[str, str]]: + """:py:class:`str` or :py:class:`list`: Returns the unique firm reference number (FRN) of a given firm, if found, or else a JSON array of matching records. Calls the private method :py:meth:`~financial_services_register_api.FinancialServicesRegisterApiClient._search_ref_number` to do the search. Returns a non-null string of the FRN if there is a unique associated - firm. + firm. Otherwise, a JSON array of all matching records is returned. Parameters ---------- firm_name : str - The firm name - need not be in any particular case. The name - needs to be precise enough to guarantee a unique return value, - otherwise multiple records exist and an exception is raised. + The firm name (case insensitive). The name needs to be precise + enough to guarantee a unique return value, otherwise a JSON array + of all matching records are returned. Returns ------- str - A string version of the firm reference number (FRN), if found. + A string version of the firm reference number (FRN), if found, or + a JSON array of all matching records. Examples -------- @@ -447,22 +451,15 @@ def search_frn(self, firm_name: str) -> str: '311492' >>> client.search_frn('hiscox insurance company limited') '113849' - >>> client.search_frn('direct line') - Traceback (most recent call last): - ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: Multiple firms returned. The firm name needs to be more precise. If you are unsure of the results please use the common search endpoint. - >>> client.search_frn('direct line insurance') - Traceback (most recent call last): - ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: Multiple firms returned. The firm name needs to be more precise. If you are unsure of the results please use the common search endpoint. - >>> client.search_frn('direct line insurance plc') - '202684' - >>> client.search_frn('Hiscxo Insurance Company') - Traceback (most recent call last): - ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: No data found in FS Register API response. Please check the search parameters and try again. + >>> res = client.search_frn('direct line') + >>> assert isinstance(res, list) + >>> assert all(isinstance(rec, dict) for rec in res) >>> client.search_frn('hiscox insurance company') '113849' + >>> client.search_frn('nonexistent company') + Traceback (most recent call last): + ... + financial_services_register_api.exceptions.FinancialServicesRegisterApiRequestException: No data found in the API response. Please check the search parameters and try again. """ return self._search_ref_number( firm_name, @@ -1160,41 +1157,29 @@ def get_firm_appointed_representatives(self, frn: str) -> FinancialServicesRegis modifiers=('AR',) ) - def search_irn(self, individual_name: str) -> str: - """:py:class:`str`: Returns the unique individual reference number (IRN) of a given individual, if found. - - Uses the API common search endpoint: - :: - - /V0.1/Search?q=&type=individual + def search_irn(self, individual_name: str) -> str | list[dict[str, str]]: + """:py:class:`str` or :py:class:`list`: Returns the unique individual reference number (IRN) of a given individual, if found, or else a JSON array of matching records. - to perform a case-insensitive individual-type search in the FS Register on the - given name. + Calls the private method + :py:meth:`~financial_services_register_api.FinancialServicesRegisterApiClient._search_ref_number` + to do the search. Returns a non-null string of the IRN if there is a unique associated - individual. + individual. Otherwise, a JSON array of all matching records is + returned. Parameters ---------- - individual_name : str - The individual name - need not be in any particular case. The name - needs to be precise enough to guarantee a unique return value, - otherwise multiple records exist and an exception is raised. - - Raises - ------ - FinancialServicesRegisterApiRequestException - If there was a request exception from calling the common search - handler. - - FinancialServicesRegisterApiException - If there was an error in the API response or in processing the response. + firm_name : str + The individual name (case insensitive). The name needs to be precise + enough to guarantee a unique return value, otherwise a JSON array + of all matching records are returned. Returns ------- str - A string version of the individual reference number (IRN), if - found. + A string version of the individual reference number (IRN), if found, or + a JSON array of all matching records. Examples -------- @@ -1204,14 +1189,13 @@ def search_irn(self, individual_name: str) -> str: 'MXC29012' >>> client.search_irn('mark Carney') 'MXC29012' - >>> client.search_irn('Mark C') + >>> res = client.search_irn('Mark C') + >>> assert isinstance(res, list) + >>> assert all(isinstance(rec, dict) for rec in res) + >>> client.search_irn('nonexistent individual') Traceback (most recent call last): ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: Multiple individuals returned. The individual name needs to be more precise. If you are unsure of the results please use the common search endpoint. - >>> client.search_irn('A Nonexistent Person') - Traceback (most recent call last): - ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: No data found in FS Register API response. Please check the search parameters and try again. + financial_services_register_api.exceptions.FinancialServicesRegisterApiRequestException: No data found in the API response. Please check the search parameters and try again. """ return self._search_ref_number( individual_name, @@ -1336,60 +1320,42 @@ def get_individual_disciplinary_history(self, irn: str) -> FinancialServicesRegi modifiers=('DisciplinaryHistory',) ) - def search_prn(self, fund_name: str) -> str: - """:py:class:`str` : Returns the unique product reference number (PRN) of a given fund or collective investment scheme (CIS), including subfunds, if it exists. - - Uses the API common search endpoint: - :: - - /V0.1/Search?q=&type=fund + def search_prn(self, fund_name: str) -> str | list[dict[str, str]]: + """:py:class:`str` or :py:class:`list`: Returns the unique product reference number (PRN) of a given fund, if found, or else a JSON array of matching records. - to perform a case-insensitive fund-type search in the FS Register on - the given name. + Calls the private method + :py:meth:`~financial_services_register_api.FinancialServicesRegisterApiClient._search_ref_number` + to do the search. Returns a non-null string of the PRN if there is a unique associated - fund. + fund. Otherwise, a JSON array of all matching records is returned. Parameters ---------- - fund_name : str - The fund name - need not be in any particular case. The name needs - to be precise enough to guarantee a unique return value, otherwise - multiple records exist and an exception is raised. - - Raises - ------ - FinancialServicesRegisterApiRequestException - If there was a request exception from calling the common search - handler. - - FinancialServicesRegisterApiResponseException - If there was an error in the API response or in processing the - response. + firm_name : str + The fund name (case insensitive). The name needs to be precise + enough to guarantee a unique return value, otherwise a JSON array + of all matching records are returned. Returns ------- str - A string version of the product reference number (PRN), if found. + A string version of the product reference number (PRN), if found, or + a JSON array of all matching records. Examples -------- >>> import os >>> client = FinancialServicesRegisterApiClient(os.environ['API_USERNAME'], os.environ['API_KEY']) - >>> client.search_prn('Northern Trust') - Traceback (most recent call last): - ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: Multiple funds returned. The fund name needs to be more precise. If you are unsure of the results please use the common search endpoint. - >>> client.search_prn('Northern Trust High Dividend ESG World Equity') - Traceback (most recent call last): - ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: Multiple funds returned. The fund name needs to be more precise. If you are unsure of the results please use the common search endpoint. >>> client.search_prn('Northern Trust High Dividend ESG World Equity Feeder Fund') '913937' - >>> client.search_prn('A nonexistent fund') + >>> res = client.search_prn('Northern Trust') + >>> assert isinstance(res, list) + >>> assert all(isinstance(rec, dict) for rec in res) + >>> client.search_prn('nonexistent fund') Traceback (most recent call last): ... - financial_services_register_api.exceptions.FinancialServicesRegisterApiResponseException: No data found in FS Register API response. Please check the search parameters and try again. + financial_services_register_api.exceptions.FinancialServicesRegisterApiRequestException: No data found in the API response. Please check the search parameters and try again. """ return self._search_ref_number( fund_name, @@ -1593,7 +1559,7 @@ def get_regulated_markets(self) -> FinancialServicesRegisterApiResponse: if __name__ == "__main__": # pragma: no cover # Doctest the module from the project root using # - # export API_USERNAME= && export API_KEY= && python -m doctest -v src/financial_services_register_api/api.py && unset API_USERNAME && unset API_KEY + # export API_USERNAME= && export API_KEY= && PYTHONPATH=src python -m doctest -v src/financial_services_register_api/api.py && unset API_USERNAME && unset API_KEY # import doctest doctest.testmod() diff --git a/src/financial_services_register_api/constants.py b/src/financial_services_register_api/constants.py index cb9f64e..ea25b20 100644 --- a/src/financial_services_register_api/constants.py +++ b/src/financial_services_register_api/constants.py @@ -26,6 +26,7 @@ class FINANCIAL_SERVICES_REGISTER_API_CONSTANTS(Enum): API_VERSION = 'V0.1' BASEURL = f'https://register.fca.org.uk/services/{API_VERSION}' + DEVELOPER_PORTAL = 'https://register.fca.org.uk/Developer/s/' RESOURCE_TYPES = { 'firm': {'type_name': 'firm', 'endpoint_base': 'Firm'}, 'fund': {'type_name': 'fund', 'endpoint_base': 'CIS'}, diff --git a/tests/units/test_api.py b/tests/units/test_api.py index 36530fa..5f01669 100644 --- a/tests/units/test_api.py +++ b/tests/units/test_api.py @@ -126,16 +126,16 @@ def test_financial_services_register_api_client___search_ref_number__exceptional test_client._search_ref_number('exceptional search', 'individual') test_client._search_ref_number('exceptional search', 'fund') - def test_financial_services_register_api_client___search_ref_number__response_not_ok__api_response_exception_raised(self): + def test_financial_services_register_api_client___search_ref_number__response_not_ok__api_request_exception_raised(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) with mock.patch('financial_services_register_api.api.FinancialServicesRegisterApiClient.common_search', return_value=mock.MagicMock(ok=False)): - with pytest.raises(FinancialServicesRegisterApiResponseException): + with pytest.raises(FinancialServicesRegisterApiRequestException): test_client._search_ref_number('exceptional search', 'firm') test_client._search_ref_number('exceptional search', 'individual') test_client._search_ref_number('exceptional search', 'fund') - def test_financial_services_register_api_client___search_ref_number__no_fs_register_data_in_response__api_response_exception_raised(self): + def test_financial_services_register_api_client___search_ref_number__no_fs_register_data_in_response__api_request_exception_raised(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) with mock.patch('financial_services_register_api.api.FinancialServicesRegisterApiSession.get') as mock_api_session_get: @@ -143,25 +143,12 @@ def test_financial_services_register_api_client___search_ref_number__no_fs_regis mock_response.json = mock.MagicMock(name='json', return_value=dict()) mock_api_session_get.return_value = mock_response - with pytest.raises(FinancialServicesRegisterApiResponseException): - test_client._search_ref_number('exceptional search', 'firm') - test_client._search_ref_number('exceptional search', 'individual') - test_client._search_ref_number('exceptional search', 'fund') - - def test_financial_services_register_api_client___search_ref_number__fs_register_data_with_index_error__api_response_exception_raised(self): - test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) - - with mock.patch('financial_services_register_api.api.FinancialServicesRegisterApiSession.get') as mock_api_session_get: - mock_response = mock.create_autospec(requests.Response) - mock_response.json = mock.MagicMock(name='json', return_value={'Data': []}) - mock_api_session_get.return_value = mock_response - - with pytest.raises(FinancialServicesRegisterApiResponseException): - test_client._search_ref_number('exceptional search', 'firm') - test_client._search_ref_number('exceptional search', 'individual') - test_client._search_ref_number('exceptional search', 'fund') + with pytest.raises(FinancialServicesRegisterApiRequestException): + test_client._search_ref_number('bad search', 'firm') + test_client._search_ref_number('bad search', 'individual') + test_client._search_ref_number('bad search', 'fund') - def test_financial_services_register_api_client___search_ref_number__fs_register_data_with_key_error__api_response_exception_raised(self): + def test_financial_services_register_api_client___search_ref_number__fs_register_data_with_key_error__api_request_exception_raised(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) with mock.patch('financial_services_register_api.api.FinancialServicesRegisterApiSession.get') as mock_api_session_get: @@ -170,23 +157,23 @@ def test_financial_services_register_api_client___search_ref_number__fs_register mock_api_session_get.return_value = mock_response with pytest.raises(FinancialServicesRegisterApiResponseException): - test_client._search_ref_number('exceptional search', 'firm') - test_client._search_ref_number('exceptional search', 'individual') - test_client._search_ref_number('exceptional search', 'fund') + test_client._search_ref_number('bad response', 'firm') + test_client._search_ref_number('bad response', 'individual') + test_client._search_ref_number('bad response', 'fund') def test_financial_services_register_api_client___search_ref_number__incorrectly_specified_resource__no_fs_register_data__api_response_exception_raised(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) # Covers the case of a failed FRN search for an incorrectly specified firm - with pytest.raises(FinancialServicesRegisterApiResponseException): + with pytest.raises(FinancialServicesRegisterApiRequestException): test_client._search_ref_number('nonexistent123 insurance company', 'firm') # Covers the case of a failed IRN search for an incorrectly specified individual - with pytest.raises(FinancialServicesRegisterApiResponseException): + with pytest.raises(FinancialServicesRegisterApiRequestException): test_client._search_ref_number('a nonexistent individual', 'individual') # Covers the case of a failed PRN search for an incorrectly specified firm - with pytest.raises(FinancialServicesRegisterApiResponseException): + with pytest.raises(FinancialServicesRegisterApiRequestException): test_client._search_ref_number('a nonexistent fund', 'fund') def test_financial_services_register_api_client___search_ref_number__inadequately_specified_resource__nonunique_fs_register_data__api_response_exception_raised(self): @@ -194,18 +181,21 @@ def test_financial_services_register_api_client___search_ref_number__inadequatel # Covers the case of an FRN search based on an inadequately specified firm # that produces multiple results - with pytest.raises(FinancialServicesRegisterApiResponseException): - test_client._search_ref_number('direct line', 'firm') + recv_recs = test_client._search_ref_number('direct line', 'firm') + assert isinstance(recv_recs, list) + assert all(isinstance(rec, dict) for rec in recv_recs) # Covers the case of an IRN search based on an inadequately specified individual # that produces multiple results - with pytest.raises(FinancialServicesRegisterApiResponseException): - test_client._search_ref_number('john smith', 'individual') + recv_recs = test_client._search_ref_number('john smith', 'individual') + assert isinstance(recv_recs, list) + assert all(isinstance(rec, dict) for rec in recv_recs) # Covers the case of an PRN search based on an inadequately specified firm # that produces multiple results - with pytest.raises(FinancialServicesRegisterApiResponseException): - test_client._search_ref_number('jupiter', 'fund') + recv_recs = test_client._search_ref_number('jupiter', 'fund') + assert isinstance(recv_recs, list) + assert all(isinstance(rec, dict) for rec in recv_recs) def test_financial_services_register_api_client___search_ref_number__correctly_and_adequately_specced_resource__unique_fs_register_data__response_returned_ok(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) @@ -241,6 +231,18 @@ def test_financial_services_register_api_client___search_frn__correctly_and_adeq assert isinstance(recv_frn, str) assert recv_frn + def test_financial_services_register_api_client___search_frn__inadequately_specced_firm__json_array_of_matching_records__response_returned_ok(self): + test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) + + # Covers the case of a successful FRN search for existing, unique firms + recv_recs = test_client._search_ref_number('hsbc', 'firm') + assert isinstance(recv_recs, list) + assert all(isinstance(rec, dict) for rec in recv_recs) + + recv_recs = test_client._search_ref_number('northern', 'firm') + assert isinstance(recv_recs, list) + assert all(isinstance(rec, dict) for rec in recv_recs) + def test_financial_services_register_api_client___get_resource_info__invalid_resource_type__no_modifiers__value_error_raised(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) @@ -837,6 +839,14 @@ def test_financial_services_register_api_client___search_irn__correctly_and_adeq assert isinstance(recv_irn, str) assert recv_irn + def test_financial_services_register_api_client___search_irn__inadequately_specced_individual__json_array_of_matching_records__response_returned_ok(self): + test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) + + # Covers the case of a successful IRN search for existing, unique individuals + recv_recs = test_client.search_irn('john smith') + assert isinstance(recv_recs, list) + assert (isinstance(rec, dict) for rec in recv_recs) + def test_financial_services_register_api_client__get_individual(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) @@ -898,6 +908,14 @@ def test_financial_services_register_api_client___search_prn__correctly_and_adeq assert isinstance(recv_prn, str) assert recv_prn + def test_financial_services_register_api_client___search_prn__inadequately_specced_fund__json_array_of_matching_records__response_returned_ok(self): + test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) + + # Covers the case of a successful PRN search for existing, unique funds + recv_recs = test_client.search_prn('jupiter') + assert isinstance(recv_recs, list) + assert all(isinstance(rec, dict) for rec in recv_recs) + def test_financial_services_register_api_client__get_fund(self): test_client = FinancialServicesRegisterApiClient(self._api_username, self._api_key) From 8b4f17550a636d1dda9b9ad4935af1e09437c1e7 Mon Sep 17 00:00:00 2001 From: Sandeep Murthy Date: Sat, 13 Sep 2025 18:37:43 +0100 Subject: [PATCH 3/3] Fix README badges --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 360948d..393efd2 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,8 @@ [![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) [![Docs](https://readthedocs.org/projects/financial-services-register-api/badge/?version=latest)](https://financial-services-register-api.readthedocs.io/en/latest/?badge=latest) - -trackgit-views - [![PyPI version](https://img.shields.io/pypi/v/financial-services-register-api?logo=python&color=41bb13)](https://pypi.org/project/financial-services-register-api) -![PyPI Downloads](https://static.pepy.tech/badge/financial-services-register-api) +![PyPI Downloads](https://static.pepy.tech/badge/fsrapiclient)