diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..3bfabfc --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,36 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1e1ec28 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + - push + - pull_request + +jobs: + test: + runs-on: ${{ matrix.os}} + strategy: + matrix: + os: [windows-latest] + python-version: ['3.9'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + echo '${{ secrets.private }}' > private.pem + - name: Test with tox + run: > + tox -- -x --user ${{ secrets.user }} + --pw ${{ secrets.pw }} + --clientId ${{ secrets.client_id }} + --tokenUrl ${{ secrets.token_url }} + --apiUrl ${{ secrets.api_url }} + --apiUrlAnalytics ${{ secrets.api_url_analytics }} + --assertionType '${{ secrets.assertion_type }}' + --scope '${{ secrets.scope }}' + --profileIdType ${{ secrets.profile_id_type }} \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md index 36a02ea..9a34ed4 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,7 +1,9 @@ # Burgiss API ## Description -This package simplifies the connection to the Burgiss API and is built on top of the requests package +This package simplifies the connection to the Burgiss API and flattens API responses to dataframes. + +![Tests](https://github.com/jfallt/burgissApi/actions/workflows/tests.yml/badge.svg) ## Authentication Setup The class burgissApiAuth handles all the JWT token authentication but there are a few prerequesite requirements for the authentication. @@ -21,7 +23,8 @@ pip install burgiss-api ``` ## Usage -Data can be updated via the api, to enable this you must change the scope in the config file and specify the request type. +### Get requests +Request method defaults to get ```python from burgissApi import burgissApiSession @@ -38,9 +41,24 @@ lookUpValues = burgissSession.request('LookupValues', profileIdAsHeader=True) # Optional Parameters investments = burgissSession.request('investments', optionalParameters='&includeInvestmentNotes=false&includeCommitmentHistory=false&includeInvestmentLiquidationNotes=false') ``` +### Put requests +Must add optional parameters for requestType and data + +```python +from burgissApi import burgissApiSession + +# Initiate a session and get profile id for subsequent calls (obtains auth token) +burgissSession = burgissApiSession() + +# When creating a put request, all fields must be present +data = {'someJsonObject':'data'} + +# Specify the request type +orgs = burgissSession.request('some endpoint', requestType='PUT', data=data) +``` ## Transformed Data Requests -Some endpoints are supported for transformation to a flattened dataframe instead of a raw json +Receive a flattened dataframe instead of a raw json from api ```python from burgissApi import burgissApi @@ -50,21 +68,6 @@ apiSession = burgissApi() orgs = apiSession.getData('orgs') ``` -
-Supported Endpoints - -|Field| -| -------| -|portfolios| -|orgs| -|orgs details| -|investments| -|investments transactions| -|LookupData| -|LookupValues| -
- - ## Analytics API ```python from burgissApi import burgissApiSession @@ -72,7 +75,16 @@ from burgissApi import burgissApiSession # Initiate a session and get profile id for subsequent calls (obtains auth token) burgissSession = burgissApiSession() +# Get grouping fields burgissSession.request('analyticsGroupingFields', analyticsApi=True, profileIdAsHeader=True) + +# Specify inputs for point in time analyis +analysisJson = pointInTimeAnalyisInput(analysisParameters, globalMeasureParameters, + measures, measureStartDateReference, measureEndDateReference, dataCriteria, groupBy) + +# Send post request to receive data +burgissSession.request('pointinTimeAnalysis', analyticsApi=True, + profileIdAsHeader=True, requestType='POST', data=analysisJson) ```
@@ -123,4 +135,4 @@ burgissSession.request('analyticsGroupingFields', analyticsApi=True, profileIdAs - [Burgiss API Documentation](https://api.burgiss.com/v2/docs/index.html) - [Burgiss Analytics API Documentation](https://api-analytics.burgiss.com/swagger/index.html) - [Burgiss API Token Auth Documentation](https://burgiss.docsend.com/view/fcqygcx) -- [Pypi Package](https://pypi.org/project/burgiss-api/) \ No newline at end of file +- [Pypi Package](https://pypi.org/project/burgiss-api/) diff --git a/burgissApi/__init__.py b/burgissApi/__init__.py deleted file mode 100644 index 2af3dd4..0000000 --- a/burgissApi/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .burgissApi import * diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..521edaf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools>=42.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +addopts = "--cov=burgissApiWrapper" +testpaths = [ + "tests", +] + +[tool.mypy] +mypy_path = "src" +check_untyped_defs = true +disallow_any_generics = false +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +strict_equality = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +no_implicit_reexport = true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 22ad5dc..f4f8cff 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/requirementsDev.txt b/requirementsDev.txt new file mode 100644 index 0000000..473f0d4 --- /dev/null +++ b/requirementsDev.txt @@ -0,0 +1,7 @@ +tox==3.24.4 +pytest==6.2.5 +pytest-cov==3.0.0 +pytest-rerunfailures==10.2 +mypy==0.910 +mypy-extensions==0.4.3 +flake8==4.0.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6c29ea0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,60 @@ +[metadata] +name = burgiss-api +version = 0.0.6 +description = An api wrapper package for financial data provided by Burgiss +long_description = A package that makes it easy to make requests to the Burgiss API by simplifying the JWT token auth. Additional functionality includes data transformations. +author = Jared Fallt +author_email = fallt.jared@gmail.com +license = MIT +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + +[options] +packages = + burgissApiWrapper +install_requires = + atomicwrites>=1.4.0 + attrs>=21.2.0 + certifi>=2021.5.30 + cffi>=1.14.6 + charset-normalizer>=2.0.4 + colorama>=0.4.4 + cryptography>=3.4.8 + idna>=3.2 + iniconfig>=1.1.1 + numpy>=1.21.2 + packaging>=21.0 + pandas>=1.3.3 + pluggy>=1.0.0 + py>=1.10.0 + pycparser>=2.20 + PyJWT>=2.1.0 + pyodbc>=4.0.32 + pyOpenSSL>=20.0.1 + pyparsing>=2.4.7 + python-dateutil>=2.8.2 + pytz>=2021.1 + requests>=2.26.0 + six>=1.16.0 + toml>=0.10.2 + urllib3>=1.26.6 +package_dir= + =src +zip_safe = no + +[options.extras_require] +testing = + pytest>=6.2.5 + pytest-cov>=3.0.0 + mypy>=0.910 + flake>=4.0.1 + tox>=3.24.4 + +[options.package_data] +burgissApiWrapper = py.typed + +[flake8] +max-line-length = 200 diff --git a/setup.py b/setup.py index 24ca8fd..57c026b 100644 --- a/setup.py +++ b/setup.py @@ -1,34 +1,4 @@ -from pkg_resources import Requirement, resource_filename -from setuptools import setup, find_packages -import pathlib from setuptools import setup -# The directory containing this file -HERE = pathlib.Path(__file__).parent - -# The text of the README file -README = (HERE / "README.md").read_text() - - -VERSION = '0.0.5' -DESCRIPTION = 'An api wrapper package for Burgiss' -LONG_DESCRIPTION = 'A package that makes it easy to make requests to the Burgiss API by simplifying the JWT token auth. Additional functionality includes data transformations.' - -setup( - name="burgiss-api", - version=VERSION, - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - author="Jared Fallt", - author_email="fallt.jared@gmail.com", - license='MIT', - packages=find_packages(), - install_requires=[], - keywords='burgiss', - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - 'License :: OSI Approved :: MIT License', - "Programming Language :: Python :: 3", - ] -) +if __name__ == "__main__": + setup() \ No newline at end of file diff --git a/src/burgissApiWrapper/__init__.py b/src/burgissApiWrapper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/burgissApi/burgissApi.py b/src/burgissApiWrapper/burgissApi.py similarity index 65% rename from burgissApi/burgissApi.py rename to src/burgissApiWrapper/burgissApi.py index 25cc9c3..23f9500 100644 --- a/burgissApi/burgissApi.py +++ b/src/burgissApiWrapper/burgissApi.py @@ -1,5 +1,4 @@ import configparser -import json import logging import uuid from datetime import datetime, timedelta @@ -11,14 +10,16 @@ from cryptography.hazmat.primitives import serialization from OpenSSL import crypto -# Create logging file for debugging -for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) - logging.basicConfig(filename='burgissApi.log', - encoding='utf-8', level=logging.DEBUG, - format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') -logger = logging.getLogger('burgissApi') -filehandler_dbg = logging.FileHandler(logger.name + '.log', mode='w') +# Create and configure logger +logging.basicConfig(filename="burgissApiWrapper.log", + format='%(asctime)s %(message)s', + filemode='w') + +# Creating an object +logger = logging.getLogger() + +# Setting the threshold of logger to DEBUG +logger.setLevel(logging.DEBUG) class ApiConnectionError(Exception): @@ -30,42 +31,82 @@ def responseCodeHandling(response): """ Handle request responses and log if there are errors """ + knownResponseCodes = {400: 'Unauthorized', 401: 'Forbidden', 404: 'Not Found', 500: 'Internal Server Error', 503: 'Service Unavailable'} if response.status_code == 200: return response - elif response.status_code == 404: + elif response.status_code in knownResponseCodes.keys(): logger.error( - "Url not found") + f"API Connection Failure: Error Code {response.status_code}, {knownResponseCodes[response.status_code]}") raise ApiConnectionError( - 'Url not found, check the logs for the specific url!') + f"Error Code {response.status_code}, {knownResponseCodes[response.status_code]}") else: - logger.error( - f"API Connection Failure: Error Code {response.status_code}") raise ApiConnectionError( 'No recognized reponse from Burgiss API, Check BurgissApi.log for details') -def lowerDictKeys(d): +def tokenErrorHandling(tokenResponseJson: dict): + # Error Handling + if 'access_token' in tokenResponseJson.keys(): + logger.info("Token request successful!") + return tokenResponseJson['access_token'] + elif 'error' in tokenResponseJson.keys(): + logging.error( + f"API Connection Error: {tokenResponseJson['error']}") + raise ApiConnectionError( + 'Check BurgissApi.log for details') + elif 'status_code' in tokenResponseJson.keys(): + logging.error( + f"API Connection Error: Error Code {tokenResponseJson ['status_code']}") + raise ApiConnectionError( + 'Check BurgissApi.log for details') + else: + logging.error("Cannot connect to endpoint") + raise ApiConnectionError( + 'No recognized reponse from Burgiss API, Check BurgissApi.log for details') + + +def lowerDictKeys(d: dict): newDict = dict((k.lower(), v) for k, v in d.items()) return newDict -class burgissApiAuth(object): +class tokenAuth(object): """ Create and send a signed client token to receive a bearer token from the burgiss api endpoint """ - def __init__(self): + def __init__(self, clientId=None, username=None, password=None, urlToken=None, urlApi=None, analyticsUrlApi=None, assertionType=None, scope=None): logger.info("Import client details from config file") config = configparser.ConfigParser() - config.read_file(open('config.cfg')) - self.clientId = config.get('API', 'clientId') - self.username = config.get('API', 'user') - self.password = config.get('API', 'pw') - self.urlToken = config.get('API', 'tokenUrl') - self.urlApi = config.get('API', 'apiUrl') - self.analyticsUrlApi = config.get('API', 'apiUrlAnalytics') - self.assertionType = config.get('API', 'assertionType') - self.scope = config.get('API', 'scope') + try: + config.read_file(open('config.cfg')) + self.clientId = config.get('API', 'clientId') + self.username = config.get('API', 'user') + self.password = config.get('API', 'pw') + self.urlToken = config.get('API', 'tokenUrl') + self.urlApi = config.get('API', 'apiUrl') + self.analyticsUrlApi = config.get('API', 'apiUrlAnalytics') + self.assertionType = config.get('API', 'assertionType') + self.scope = config.get('API', 'scope') + except Exception as e: + logging.error(e) + print('Config file not found, is it located in your cwd?') + if clientId is not None: + self.clientId = clientId + if username is not None: + self.username = username + if password is not None: + self.password = password + if urlToken is not None: + self.urlToken = urlToken + if urlApi is not None: + self.urlApi = urlApi + if analyticsUrlApi is not None: + self.analyticsUrlApi = analyticsUrlApi + if assertionType is not None: + self.assertionType = assertionType + if scope is not None: + self.scope = scope logger.info("Client details import complete!") def getBurgissApiToken(self): @@ -86,7 +127,7 @@ def getBurgissApiToken(self): headers = { 'alg': 'RS256', - 'kid': crypto.X509().digest('sha1').decode('utf-8').replace(':', ''), + 'kid': crypto.X509().digest('sha1').decode('utf-8').replace(':', ''), # type: ignore 'typ': 'JWT' } payload = { @@ -99,9 +140,13 @@ def getBurgissApiToken(self): 'aud': self.urlToken } - logger.info("Encode client assertion with jwt") - clientToken = jwt.encode( - payload, secret_key, headers=headers, algorithm='RS256') + logger.info("Encoding client assertion with jwt") + try: + clientToken = jwt.encode( + payload, secret_key, headers=headers, algorithm='RS256') # type: ignore + logger.info("Encoding complete!") + except Exception as e: + logging.error(e) payload = { 'grant_type': 'password', @@ -117,41 +162,34 @@ def getBurgissApiToken(self): tokenResponse = requests.request( 'POST', self.urlToken, data=payload ) - tokenResponseJson = tokenResponse.json() - - # Error Handling - if 'access_token' in tokenResponseJson.keys(): - logger.info("Token request successful!") - return tokenResponseJson['access_token'] - elif 'error' in tokenResponseJson.keys(): - logging.error( - f"API Connection Error: {tokenResponseJson['error']}") - raise ApiConnectionError( - 'Check BurgissApi.log for details') - elif 'status_code' in tokenResponseJson.keys(): - logging.error( - f"API Connection Error: Error Code {tokenResponseJson ['status_code']}") - raise ApiConnectionError( - 'Check BurgissApi.log for details') - else: - logging.error("Cannot connect to endpoint") - raise ApiConnectionError( - 'No recognized reponse from Burgiss API, Check BurgissApi.log for details') + return tokenErrorHandling(tokenResponse.json()) -class burgissApiInit(burgissApiAuth): +class init(tokenAuth): """ - Initializes a session for all subsequent calls using the burgissApiAuth class + Initializes a session for all subsequent calls using the tokenAuth class """ - def __init__(self): - self.auth = burgissApiAuth() + def __init__(self, clientId=None, username=None, password=None, urlToken=None, urlApi=None, analyticsUrlApi=None, assertionType=None, scope=None): + self.auth = tokenAuth(clientId, username, password, urlToken, urlApi, analyticsUrlApi, assertionType, scope) self.token = self.auth.getBurgissApiToken() self.tokenExpiration = datetime.utcnow() + timedelta(seconds=3600) self.urlApi = self.auth.urlApi self.analyticsUrlApi = self.auth.analyticsUrlApi - def request(self, url: str, analyticsApi: bool = False, requestType: str = 'GET', profileIdHeader: bool = False, data=''): + def checkTokenExpiration(self): + """ + Check if token is expired, if it is get a new token + """ + logger.info('Check if token has expired') + if self.tokenExpiration < datetime.utcnow(): + logger.info('Token has expired, getting new token') + self.token = self.auth.getBurgissApiToken() + self.tokenExpiration = datetime.utcnow() + timedelta(seconds=3600) + else: + logger.info('Token is still valid') + + def requestWrapper(self, url: str, analyticsApi: bool = False, requestType: str = 'GET', profileIdHeader: bool = False, data=''): """ Burgiss api request call, handling bearer token auth in the header with token received when class initializes @@ -163,18 +201,10 @@ def request(self, url: str, analyticsApi: bool = False, requestType: str = 'GET' Returns: Response [json]: Data from url input """ - - # Check if token is expired, if it is get a new token - logger.info('Check if token has expired') - if self.tokenExpiration < datetime.utcnow(): - logger.info('Token has expired, getting new token') - self.token = self.auth.getBurgissApiToken() - self.tokenExpiration = datetime.utcnow() + timedelta(seconds=3600) - else: - logger.info('Token is still valid') + self.checkTokenExpiration() # Default to regular api but allow for analytics url - if analyticsApi == False: + if analyticsApi is False: baseUrl = self.urlApi else: baseUrl = self.analyticsUrlApi @@ -199,26 +229,31 @@ def request(self, url: str, analyticsApi: bool = False, requestType: str = 'GET' return responseCodeHandling(response) -class burgissApiSession(burgissApiInit): +class session(init): """ Simplifies request calls by getting auth token and profile id from parent classes """ - def __init__(self): + def __init__(self, clientId=None, username=None, password=None, urlToken=None, urlApi=None, analyticsUrlApi=None, assertionType=None, scope=None, profileIdType=None): """ Initializes a request session, authorizing with the api and gets the profile ID associated with the logged in account """ config = configparser.ConfigParser() - config.read_file(open('config.cfg')) - self.profileIdType = config.get('API', 'profileIdType') - self.session = burgissApiInit() - self.profileResponse = self.session.request( + try: + config.read_file(open('config.cfg')) + self.profileIdType = config.get('API', 'profileIdType') + except Exception as e: + logging.debug(e) + if profileIdType is not None: + self.profileIdType = profileIdType + self.session = init(clientId, username, password, urlToken, urlApi, analyticsUrlApi, assertionType, scope) + self.profileResponse = self.session.requestWrapper( 'profiles').json() self.profileId = self.profileResponse[0][self.profileIdType] def request(self, url: str, analyticsApi: bool = False, profileIdAsHeader: bool = False, optionalParameters: str = '', requestType: str = 'GET', data=''): """ - Basic request, built on top of burgissApiInit.request, which handles urls and token auth + Basic request, built on top of init.requestWrapper, which handles urls and token auth Args: url (str): Each burgiss endpoint has different key words e.g. 'investments' -> Gets list of investments @@ -235,7 +270,7 @@ def request(self, url: str, analyticsApi: bool = False, profileIdAsHeader: bool response [object]: Request object, refer to the requests package documenation for details """ - if profileIdAsHeader == False: + if profileIdAsHeader is False: profileUrl = f'?profileID={self.profileId}' profileIdHeader = False else: @@ -244,18 +279,18 @@ def request(self, url: str, analyticsApi: bool = False, profileIdAsHeader: bool endpoint = url + profileUrl + optionalParameters - response = self.session.request( + response = self.session.requestWrapper( endpoint, analyticsApi, requestType, profileIdHeader, data) return responseCodeHandling(response) -class burgissApi(): - def __init__(self): +class transformResponse(session): + def __init__(self, clientId=None, username=None, password=None, urlToken=None, urlApi=None, analyticsUrlApi=None, assertionType=None, scope=None, profileIdType=None): """ Initializes a request session, authorizing with the api and gets the profile ID associated with the logged in account """ - self.apiSession = burgissApiSession() + self.apiSession = session(clientId, username, password, urlToken, urlApi, analyticsUrlApi, assertionType, scope, profileIdType) # storing exceptions here for now until we can determine a better way to handle them self.nestedExceptions = {'LookupData': {'method': 'json_normalize', @@ -266,7 +301,7 @@ def __init__(self): {'method': 'nestedJson'} } - def parseNestedJson(self, responseJson): + def parseNestedJson(self, responseJson: dict): """ Custom nested json parser @@ -284,7 +319,7 @@ def parseNestedJson(self, responseJson): return dfTransformed - def flattenResponse(self, resp, field): + def flattenResponse(self, resp, field: str): """ The api sends a variety of responses, this function determines which parsing method to use based on the response """ @@ -308,7 +343,7 @@ def flattenResponse(self, resp, field): flatDf = pd.json_normalize(respLower) return flatDf - def columnNameClean(self, df): + def columnNameClean(self, df: pd.DataFrame): """ Removes column name prefix from unnested columns """ @@ -341,9 +376,9 @@ def getData(self, field: str, profileIdAsHeader: bool = False, OptionalParameter field, profileIdAsHeader=profileIdAsHeader, optionalParameters=OptionalParameters).json() # Flatten and clean response - flatDf = self.flattenResponse(resp, field) + flatDf = self.flattenResponse(resp, field) cleanFlatDf = self.columnNameClean(flatDf) - + return cleanFlatDf def getTransactions(self, id: int, field: str): @@ -352,7 +387,8 @@ def getTransactions(self, id: int, field: str): Args: id (int): refers to investmentID - field (str): 'transaction' model has different key words (e.g. 'valuation', 'cash', 'stock', 'fee', 'funding') -> Gets list of values for indicated investmentID + field (str): 'transaction' model has different key words (e.g. 'valuation', 'cash', 'stock', 'fee', 'funding') + -> Gets list of values for indicated investmentID Returns: json [object]: dictionary of specified field's values for investmentID @@ -382,4 +418,10 @@ def pointInTimeAnalyisInput(analysisParameters, globalMeasureParameters, measure pointInTimeAnalyis['dataCriteria'] = [dataCriteria] pointInTimeAnalyis['groupBy'] = groupBy - return json.dumps(pointInTimeAnalyis) + print(pointInTimeAnalyis) + # Remove any none or null values + # pointInTimeAnalyisProcessed = {x:y for x,y in pointInTimeAnalyis.items() if (y is not None and y!='null') } + print(pointInTimeAnalyis) + + return pointInTimeAnalyis + # return json.dumps(pointInTimeAnalyis) diff --git a/src/burgissApiWrapper/py.typed b/src/burgissApiWrapper/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..42cbdd6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,90 @@ +import configparser +from datetime import datetime, timedelta + + +import pytest +from burgissApiWrapper.burgissApi import (init, session, tokenAuth, + transformResponse) +from pandas import DataFrame + + +class testApiResponses(): + def __init__(self, clientId=None, username=None, password=None, urlToken=None, urlApi=None, analyticsUrlApi=None, assertionType=None, scope=None, profileIdType=None) -> None: + self.tokenInit = tokenAuth(clientId, username, password, urlToken, urlApi, analyticsUrlApi, assertionType, scope) + self.initSession = init(clientId, username, password, urlToken, urlApi, analyticsUrlApi, assertionType, scope) + self.burgissSession = session(clientId, username, password, urlToken, urlApi, analyticsUrlApi, assertionType, scope, profileIdType) + self.transformResponse = transformResponse(clientId, username, password, urlToken, urlApi, analyticsUrlApi, assertionType, scope, profileIdType) + + self.endpoints = ['orgs', 'investments', 'portfolios', 'assets', 'LookupData'] + + def testGetBurgissApiToken(self): + token = self.tokenInit.getBurgissApiToken() + assert len(token) != 0 + + def testTokenReset(self): + tokenExpiration = self.initSession.tokenExpiration + self.initSession.tokenExpiration = datetime.now() + timedelta(seconds=3600) + self.initSession.checkTokenExpiration() + assert tokenExpiration != self.initSession.tokenExpiration + + def testProfileRequest(self): + profileResponse = self.initSession.requestWrapper('profiles') + assert profileResponse.status_code == 200 + + def testRequestResponseCode(self, endpoint): + response = self.burgissSession.request(endpoint) + assert response.status_code == 200 + + def testOptionalParametersRequestResponseCode(self, endpoint, optionalParameters): + response = self.burgissSession.request( + endpoint, optionalParameters=optionalParameters) + assert response.status_code == 200 + + def testProfileIdAsHeaderResponse(self, endpoint): + response = self.burgissSession.request(endpoint, profileIdAsHeader=True) + assert response.status_code == 200 + + def testDataTransformation(self, endpoint): + response = self.transformResponse.getData(endpoint) + assert isinstance(response, DataFrame) is True + assert len(response) > 0 + + +def pytest_addoption(parser): + config = configparser.ConfigParser() + try: + config.read_file(open('config.cfg')) + except Exception as e: + print(e) + config.read_file(open('configTemplate.cfg')) + parser.addoption("--user", action="store", default=config.get('API', 'user')) + parser.addoption("--pw", action="store", default=config.get('API', 'pw')) + parser.addoption("--clientId", action="store", default=config.get('API', 'clientId')) + parser.addoption("--tokenUrl", action="store", default=config.get('API', 'tokenUrl')) + parser.addoption("--apiUrl", action="store", default=config.get('API', 'apiUrl')) + parser.addoption("--apiUrlAnalytics", action="store", default=config.get('API', 'apiUrlAnalytics')) + parser.addoption("--assertionType", action="store", default=config.get('API', 'assertionType')) + parser.addoption("--scope", action="store", default=config.get('API', 'scope')) + parser.addoption("--profileIdType", action="store", default=config.get('API', 'profileIdType')) + + +@pytest.fixture(scope='session') +def testApiResponsesFixture(pytestconfig): + """ + + """ + clientId = pytestconfig.getoption("clientId") + user = pytestconfig.getoption("user") + pw = pytestconfig.getoption("pw") + tokenUrl = pytestconfig.getoption("tokenUrl") + apiUrl = pytestconfig.getoption("apiUrl") + apiUrlAnalytics = pytestconfig.getoption("apiUrlAnalytics") + assertionType = pytestconfig.getoption("assertionType") + scope = pytestconfig.getoption("scope") + profileIdType = pytestconfig.getoption("profileIdType") + test = testApiResponses(clientId, user, pw, tokenUrl, apiUrl, apiUrlAnalytics, assertionType, scope, profileIdType) + + # Session + test.burgissSession.profileIdType = pytestconfig.getoption("profileIdType") + + return test diff --git a/tests/testBurgissAnalytics.py b/tests/testBurgissAnalytics.py index 3a2b22c..b876bb8 100644 --- a/tests/testBurgissAnalytics.py +++ b/tests/testBurgissAnalytics.py @@ -1,6 +1,6 @@ -from burgissApi.burgissApi import burgissApiSession, pointInTimeAnalyisInput -import pytest +from burgissApi.burgissApi import burgissApiSession +import json import os os.chdir("..") @@ -8,9 +8,11 @@ # Initialize session for subsequent tests burgissSession = burgissApiSession() -#===========================# +# ==========================# # BurgissAnalytics requests # -#===========================# +# ==========================# + + def testAnalyticsGroupingFields(): response = burgissSession.request( 'analyticsGroupingFields', analyticsApi=True, profileIdAsHeader=True) @@ -18,13 +20,12 @@ def testAnalyticsGroupingFields(): analysisParameters = { - 'userDefinedAnalysisName': 'Adjusted Ending Value', + 'userDefinedAnalysisName': 'Dingus', 'userDefinedAnalysisID': '1', - 'analysisResultType': 'Pooled', - 'calculationContext': 'default_value3', - 'analysisCurrency': 'local', - 'analysisStartDate': 'default_value3', - 'analysisEndDate': 'default_value3' + 'calculationContext': 'Investment', + 'analysisResultType': 'individual', + 'analysisCurrency': 'Base', # 'analysisStartDate': '2021-09-15T15:29:57.352Z', + # 'analysisEndDate': '2021-09-15T15:29:57.352Z' } globalMeasureParameters = { @@ -46,30 +47,73 @@ def testAnalyticsGroupingFields(): "referenceDate": "Inception" } -measures = {"rollForward": False, - "userDefinedMeasureAlias": "string", - "measureName": "IRR", - "measureStartDate": "2021-09-15T15:29:57.352Z", - "measureEndDate": "2021-09-15T15:29:57.352Z", - "indexID": "string", - "indexPremium": 0, - "decimalPrecision": 0 - } - - -dataCriteria = {"recordID": "string", - "RecordGUID": "string", - "recordContext": "investment", - "selectionSet": "string", +# measureStartDateReference = None +# measureEndDateReference = None + +measures = { # "rollForward": False, + # "userDefinedMeasureAlias": "string", + "measureName": "Valuation" + # "measureStartDate": "2021-09-15T15:29:57.352Z", + # "measureEndDate": "2021-09-15T15:29:57.352Z", + # "indexID": "string", + # "indexPremium": 0, + # "decimalPrecision": 0 +} + + +dataCriteria = {"recordID": "9991", + # "RecordGUID": "string", + "recordContext": "Portfolio", + # "selectionSet": "string", "excludeLiquidatedInvestments": False} -groupBy = ['Investment.Name'] +# groupBy = ['Investment.Name'] +groupBy = None -analysisJson = pointInTimeAnalyisInput(analysisParameters, globalMeasureParameters, - measures, measureStartDateReference, measureEndDateReference, dataCriteria, groupBy) +# analysisJson, analysisJson2 = pointInTimeAnalyisInput( +# analysisParameters, +# globalMeasureParameters, +# measures, +# measureStartDateReference, +# measureEndDateReference, +# dataCriteria, +# groupBy +# ) + +analysisJson = { + "pointInTimeAnalysis": [ + { + "userDefinedAnalysisName": "NAVreport", + "userDefinedAnalysisID": "NAVreport-001", + "calculationContext": "Investment", + "analysisResultType": "individual", + "analysisCurrency": "Base", + "globalMeasureProperties": { + "rollForward": True + }, + "measures": [ + { + "measureName": "Valuation" + + } + + ] + } + ], + "dataCriteria": [ + { + "recordID": "9991", + "recordContext": "portfolio" + + } + ] +} +print(json.dumps(analysisJson)) burgissSession = burgissApiSession() burgissSession.request('analyticsGroupingFields', analyticsApi=True, profileIdAsHeader=True) -burgissSession.request('pointinTimeAnalysis', analyticsApi=True, - profileIdAsHeader=True, requestType='POST', data=analysisJson) +boi = burgissSession.request('pointinTimeAnalysis', analyticsApi=True, + profileIdAsHeader=True, requestType='POST', data=json.dumps(analysisJson)) + +print(boi) diff --git a/tests/testBurgissApiRequests.py b/tests/testBurgissApiRequests.py deleted file mode 100644 index 960d51e..0000000 --- a/tests/testBurgissApiRequests.py +++ /dev/null @@ -1,56 +0,0 @@ - -from burgissApi.burgissApi import burgissApiSession, burgissApiInit, burgissApiAuth, ApiConnectionError, pointInTimeAnalyisInput -import pytest - -import os -os.chdir("..") - -#=======================# -# Test requests # -#=======================# - - -def testProfileRequest(): - session = burgissApiInit() - profileResponse = session.request('profiles') - assert profileResponse.status_code == 200 - - -# Initialize session for subsequent tests -burgissSession = burgissApiSession() - - -def testOrgRequest(): - response = burgissSession.request('orgs') - assert response.status_code == 200 - - -def testInvestmentsRequest(): - response = burgissSession.request('investments') - assert response.status_code == 200 - - -def testOptionalParameters(): - response = burgissSession.request( - 'investments', optionalParameters='&includeInvestmentNotes=false&includeCommitmentHistory=false&includeInvestmentLiquidationNotes=false') - assert response.status_code == 200 - - -def testPortfolioRequest(): - response = burgissSession.request('portfolios') - assert response.status_code == 200 - - -def testLookupData(): - response = burgissSession.request('LookupData') - assert response.status_code == 200 - - -def testLookupValues(): - response = burgissSession.request('LookupValues', profileIdAsHeader=True) - assert response.status_code == 200 - - -def testInvalidUrl(): - with pytest.raises(ApiConnectionError): - burgissSession.request('fakeUrl') \ No newline at end of file diff --git a/tests/testDataTransformations.py b/tests/testDataTransformations.py deleted file mode 100644 index dc3572a..0000000 --- a/tests/testDataTransformations.py +++ /dev/null @@ -1,46 +0,0 @@ - -import os - -import pandas as pd -import pytest -from burgissApi.burgissApi import burgissApi, burgissApiSession - -os.chdir("..") - -#=======================# -# Test requests # -#=======================# -burgissApiSession = burgissApi() - - -def testOrgsTransformation(): - response = burgissApiSession.getData('orgs') - assert isinstance(response, pd.DataFrame) == True - assert len(response) > 0 - - -def testInvestmentsTransformation(): - response = burgissApiSession.getData('investments') - assert isinstance(response, pd.DataFrame) == True - assert len(response) > 0 - - -def testPortfoliosTransformation(): - response = burgissApiSession.getData('portfolios') - assert isinstance(response, pd.DataFrame) == True - assert len(response) > 0 - - -def testLookupValuesTransformation(): - response = burgissApiSession.getData( - 'LookupValues', profileIdAsHeader=True) - assert isinstance(response, pd.DataFrame) == True - assert len(response) > 0 - assert len(response.columns) == 4 - - -def testLookupDataTransformation(): - response = burgissApiSession.getData( - 'LookupData') - assert isinstance(response, pd.DataFrame) == True - assert len(response) > 0 diff --git a/tests/testTokenGen.py b/tests/testTokenGen.py deleted file mode 100644 index 97400ef..0000000 --- a/tests/testTokenGen.py +++ /dev/null @@ -1,14 +0,0 @@ - -from burgissApi.burgissApi import burgissApiAuth -import pytest - -import os -os.chdir("..") - -#=======================# -# Test token gen # -#=======================# -def testGetBurgissApiToken(): - tokenInit = burgissApiAuth() - token = tokenInit.getBurgissApiToken() - assert len(token) != 0 diff --git a/tests/test_responses.py b/tests/test_responses.py new file mode 100644 index 0000000..0b0ebeb --- /dev/null +++ b/tests/test_responses.py @@ -0,0 +1,83 @@ +import pytest +from burgissApiWrapper.burgissApi import (ApiConnectionError, + responseCodeHandling, + tokenErrorHandling, + ) +from requests.models import Response + + +def testTokenGen(testApiResponsesFixture): + testApiResponsesFixture.testGetBurgissApiToken() + + +def testTokenExpiration(testApiResponsesFixture): + testApiResponsesFixture.testTokenReset() + + +def testGetProfile(testApiResponsesFixture): + testApiResponsesFixture.testProfileRequest() + + +def testOptionalParameters(testApiResponsesFixture): + testApiResponsesFixture.testOptionalParametersRequestResponseCode('investments', '&includeInvestmentNotes=false&includeCommitmentHistory=false&includeInvestmentLiquidationNotes=false') + + +@pytest.mark.skip(reason="This endpoint has been problematic, unclear why it keeps failing") +def testProfileIdAsHeader(testApiResponsesFixture): + testApiResponsesFixture.testProfileIdAsHeaderResponse('LookupValues') + + +endpoints = [ + 'orgs', + 'investments', + 'portfolios', + 'assets', + # 'LookupData' another problematic endpoint, removing for now 2021.10.22 +] + + +@pytest.mark.flaky(reruns=5) +@pytest.mark.parametrize('endpoint', endpoints) +def testEndpoints(endpoint, testApiResponsesFixture): + """ + Test if endpoint returns a 200 status code + """ + testApiResponsesFixture.testRequestResponseCode(endpoint) + + +@pytest.mark.flaky(reruns=5) +@pytest.mark.parametrize('endpoint', endpoints) +def testDataTransformation(endpoint, testApiResponsesFixture): + "Test if endpoint returns a flattened dataframe with length > 0" + testApiResponsesFixture.testDataTransformation(endpoint) + + +# Test token response handling +validTokenExample = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' + +exampleTokenResponse = [ + {'error': 'invalid_scope'}, + {'status_code': 500}, + {'some_other_key': 'data'} +] + + +def testTokenResponse(): + assert tokenErrorHandling({'access_token': validTokenExample}) == validTokenExample + + +@pytest.mark.parametrize('tokenResponseJson', exampleTokenResponse) +def testTokenExceptions(tokenResponseJson): + with pytest.raises(ApiConnectionError): + tokenErrorHandling(tokenResponseJson) + + +responseCodes = [400, 401, 404, 500, 503] + + +@pytest.mark.parametrize('responseCode', responseCodes) +def testResponseErrorHandling(responseCode): + response = Response() + response.status_code = responseCode + with pytest.raises(ApiConnectionError): + responseCodeHandling(response) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4188550 --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +[tox] +minversion = 3.24.4 +envlist = py39, flake8, mypy +isolated_build = true + +[gh-actions] +python = + 3.9: py39, mypy, flake8 + +[testenv] +setenv = + PYTHONPATH = {toxinidir} +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirementsDev.txt +commands = + pytest --basetemp={envtmpdir} {posargs} + +[testenv:flake8] +basepython = python3.9 +deps = flake8 +commands = flake8 src tests + +[testenv:mypy] +basepython = python3.9 +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirementsDev.txt +commands = mypy src tests \ No newline at end of file