allphins-sdk 1.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.1
2
+ Name: allphins-sdk
3
+ Version: 1.0.4
4
+ Summary:
5
+ Author: Florent
6
+ Author-email: florent@allphins.com
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: pandas (>=2.1.4,<3.0.0)
13
+ Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
14
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
15
+ Requires-Dist: tqdm (>=4.66.2,<5.0.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # allphins-sdk
19
+ Allphins SKD repository
20
+
21
+ ## How to contribute
22
+
23
+ 1) Install pre-commit hook
24
+ ```bash
25
+ > pre-commit install
26
+ ```
27
+
28
+ 2) Convention followed
29
+
30
+ - [PEP8](https://www.python.org/dev/peps/pep-0008/) -> 120 characters per line
31
+
32
+ - [Google Python Style Docstring]
33
+
34
+ ## Requirements
35
+ * [Poetry](https://python-poetry.org/docs/#installation)
36
+ * [Docker](https://docs.docker.com/get-docker/)
37
+ * [Docker-compose](https://docs.docker.com/compose/install/)
38
+
39
+ ## How to set up poetry environment
40
+ To set up your development environment using poetry you have to install all the dependencies using:
41
+
42
+ ```
43
+ poetry install --with dev
44
+ ```
45
+
46
+ You can also run the documentation on a local environment:
47
+
48
+ ```
49
+ poetry run mkdocs serve
50
+ ```
51
+ The documentation will be running on your localhost port 8000
52
+
53
+ Finally, you can run the unit test using the following command:
54
+
55
+ ```
56
+ poetry run coverage run --source=./allphins tests && poetry run coverage report --fail-under=80
57
+ ```
58
+
59
+ ## Mock allphins-api
60
+ You can mock the allphins-api by using the following command:
61
+ ```
62
+ docker-compose up -d
63
+ ```
64
+ All calls to localhost:8080 will be redirected to the mock server.
65
+ You can update the mock server by updating the files in the stubs/allphins-api folder.
66
+
67
+ This is required before runing local tests.
@@ -0,0 +1,50 @@
1
+ # allphins-sdk
2
+ Allphins SKD repository
3
+
4
+ ## How to contribute
5
+
6
+ 1) Install pre-commit hook
7
+ ```bash
8
+ > pre-commit install
9
+ ```
10
+
11
+ 2) Convention followed
12
+
13
+ - [PEP8](https://www.python.org/dev/peps/pep-0008/) -> 120 characters per line
14
+
15
+ - [Google Python Style Docstring]
16
+
17
+ ## Requirements
18
+ * [Poetry](https://python-poetry.org/docs/#installation)
19
+ * [Docker](https://docs.docker.com/get-docker/)
20
+ * [Docker-compose](https://docs.docker.com/compose/install/)
21
+
22
+ ## How to set up poetry environment
23
+ To set up your development environment using poetry you have to install all the dependencies using:
24
+
25
+ ```
26
+ poetry install --with dev
27
+ ```
28
+
29
+ You can also run the documentation on a local environment:
30
+
31
+ ```
32
+ poetry run mkdocs serve
33
+ ```
34
+ The documentation will be running on your localhost port 8000
35
+
36
+ Finally, you can run the unit test using the following command:
37
+
38
+ ```
39
+ poetry run coverage run --source=./allphins tests && poetry run coverage report --fail-under=80
40
+ ```
41
+
42
+ ## Mock allphins-api
43
+ You can mock the allphins-api by using the following command:
44
+ ```
45
+ docker-compose up -d
46
+ ```
47
+ All calls to localhost:8080 will be redirected to the mock server.
48
+ You can update the mock server by updating the files in the stubs/allphins-api folder.
49
+
50
+ This is required before runing local tests.
@@ -0,0 +1,14 @@
1
+ """___ _ _________ _______ _______ _______ _______
2
+ ( ____ \( \ \__ __/( ____ )( ____ )( ____ \( ____ )
3
+ | ( \/| ( ) ( | ( )|| ( )|| ( \/| ( )|
4
+ | (__ | | | | | (____)|| (____)|| (__ | (____)|
5
+ | __) | | | | | _____)| _____)| __) | __)
6
+ | ( | | | | | ( | ( | ( | (\ (
7
+ | ) | (____/\___) (___| ) | ) | (____/\| ) \ \__
8
+ |/ (_______/\_______/|/ |/ (_______/|/ \__/
9
+
10
+ """
11
+
12
+ from allphins.interface import get_policies # noqa: F401
13
+ from allphins.interface import get_portfolios # noqa: F401
14
+ from allphins.interface import get_risks # noqa: F401
@@ -0,0 +1,5 @@
1
+ """Get resources from Allphins API."""
2
+
3
+ from allphins.client.client import Client
4
+
5
+ __all__ = ["Client"]
@@ -0,0 +1,101 @@
1
+ """Authentication class for the Allphins API."""
2
+ import os
3
+ from typing import Optional
4
+
5
+ import requests
6
+ from requests import HTTPError
7
+
8
+ from allphins.const import ACCESS_TOKEN_URL
9
+ from allphins.const import REFRESH_TOKEN_URL
10
+ from allphins.status import HTTP_401_UNAUTHORIZED
11
+
12
+
13
+ class Auth:
14
+ """Authentication class for the Allphins API.
15
+
16
+ This class manages the access and refresh tokens.
17
+ The credentials are provided in the constructor or via environment variables.
18
+
19
+ Attributes:
20
+ _access_token: The access token.
21
+ _refresh_token: The refresh token.
22
+ """
23
+
24
+ REQUEST_TIMEOUT = 3
25
+
26
+ def __init__(self, username: Optional[str] = None, password: Optional[str] = None):
27
+ """Initialize the authentication object.
28
+
29
+ Credentials are extracted from method parameters in priority and then from environment variable.
30
+
31
+ Args:
32
+ username: (Optional str) The username to use for authentication.
33
+ password: (Optional str) The password to use for authentication.
34
+
35
+ Raises:
36
+ ValueError: If the username or password is not provided.
37
+ """
38
+ self._access_token: Optional[str] = None
39
+ self._refresh_token: Optional[str] = None
40
+
41
+ username = os.environ.get('ALLPHINS_APIKEY') if username is None else username
42
+ password = os.environ.get('ALLPHINS_PASSKEY') if password is None else password
43
+ error_message = '{credential} must be provided. You can setup the {env_var} environment variable.'
44
+
45
+ if not username:
46
+ raise ValueError(error_message.format(credential='Username', env_var='ALLPHINS_USER'))
47
+
48
+ if not password:
49
+ raise ValueError(error_message.format(credential='Password', env_var='ALLPHINS_PASSWORD'))
50
+
51
+ self.username = username
52
+ self.password = password
53
+
54
+ self._get_tokens()
55
+
56
+ def _get_tokens(self):
57
+ """Get the access and refresh token.
58
+
59
+ This method is called in the constructor to get the tokens.
60
+
61
+ Raises:
62
+ ValueError: If the username or password is invalid.
63
+ HTTPError: If the request fails.
64
+ """
65
+ json = {
66
+ 'username': self.username,
67
+ 'password': self.password,
68
+ }
69
+ response = requests.post(ACCESS_TOKEN_URL, json=json, timeout=self.REQUEST_TIMEOUT)
70
+ try:
71
+ response.raise_for_status()
72
+ except HTTPError as e:
73
+ if response.status_code == HTTP_401_UNAUTHORIZED:
74
+ raise ValueError('Invalid username or password')
75
+ else:
76
+ raise e
77
+ response_json = response.json()
78
+ self._access_token = response_json['access']
79
+ self._refresh_token = response_json['refresh']
80
+
81
+ def refresh_access_token(self):
82
+ """Refresh the access token.
83
+
84
+ Raises:
85
+ HTTPError: If the request fails.
86
+ """
87
+ json = {
88
+ 'refresh': self._refresh_token,
89
+ }
90
+ response = requests.post(REFRESH_TOKEN_URL, json=json, timeout=self.REQUEST_TIMEOUT)
91
+ response.raise_for_status()
92
+ self._access_token = response.json()['access']
93
+
94
+ def get_authentication_header(self) -> dict:
95
+ """Get the authentication header to be added in any requests.
96
+
97
+ Returns:
98
+ The authentication header.
99
+ Example: {'Authorization': 'Bearer xxxx'}.
100
+ """
101
+ return {'Authorization': f'Bearer {self._access_token}', 'User-Agent': 'Allphins SDK/1.0.4'}
@@ -0,0 +1,176 @@
1
+ """Base class for client used to interact with Allphins API."""
2
+ from typing import Any
3
+ from typing import Optional
4
+
5
+ import requests
6
+ from requests import HTTPError
7
+ from requests import Response
8
+ from tqdm import tqdm
9
+
10
+ from allphins.client.auth import Auth
11
+ from allphins.singleton import Singleton
12
+ from allphins.status import HTTP_403_FORBIDDEN
13
+
14
+
15
+ class Client(metaclass=Singleton):
16
+ """Client to bundle configuration needed for API requests.
17
+
18
+ The Client is a singleton. It is instantiated once and then reused, it's to avoid requesting different tokens
19
+ for every type of objects using a :class:`Client`
20
+
21
+
22
+ Attributes:
23
+ auth: The authentication object to use for authentication.
24
+ """
25
+
26
+ REQUEST_TIMEOUT = 180 # seconds
27
+
28
+ def __init__(self, auth: Optional[Auth] = None):
29
+ """Initialize the client.
30
+
31
+ If no value is passed in for auth, a :class:`Auth` instance will be created with credentials read
32
+ via environment variables.
33
+
34
+ Args:
35
+ auth: (Optional) The authentication object to use for authentication.
36
+ """
37
+ if auth is None:
38
+ auth = Auth()
39
+ self.auth = auth
40
+
41
+ def call_api(
42
+ self,
43
+ url: str,
44
+ method: str,
45
+ json: Optional[dict] = None,
46
+ headers: Optional[dict] = None,
47
+ page_size: Optional[int] = None,
48
+ ) -> Any:
49
+ """Call the API.
50
+
51
+ Access token is automatically manged by the Auth class.
52
+ If the token is expired, it will be refreshed and the call will be retried.
53
+
54
+ Args:
55
+ url: (str) The url to call.
56
+ method: (str) The method to use for the request (GET, POST, PUT, DELETE...).
57
+ json: (Optional dict) The json to send with the request.
58
+ headers: (Optional dict) The headers to send with the request.
59
+ page_size: (Optional int) Specify the page size for the pagination. None means no pagination.
60
+
61
+ Returns:
62
+ Any: The response from the API.
63
+
64
+ Raises:
65
+ HTTPError: If the request fails.
66
+ """
67
+ response = self._perform_call(url, method, json, headers, page_size)
68
+ try:
69
+ response.raise_for_status()
70
+ except HTTPError as e:
71
+ if response.status_code == HTTP_403_FORBIDDEN:
72
+ self.auth.refresh_access_token()
73
+ response = self._perform_call(url, method, json, headers, page_size)
74
+ response.raise_for_status()
75
+ else:
76
+ raise e
77
+ return response.json()
78
+
79
+ def call_api_with_pagination(
80
+ self,
81
+ url: str,
82
+ method: str,
83
+ page_size: int,
84
+ json: Optional[dict] = None,
85
+ headers: Optional[dict] = None,
86
+ ) -> list:
87
+ """Call the API with pagination.
88
+
89
+ Automatically iterate through the pages to get all the results.
90
+ Display progress bar based on the number of results.
91
+ Access token is automatically manged by the Auth class.
92
+ If the token is expired, it will be refreshed and the call will be retried.
93
+
94
+ Args:
95
+ url: (str) The url to call.
96
+ method: (str) The method to use for the request (GET, POST, PUT, DELETE...).
97
+ page_size: (int) Specify the page size for the pagination.
98
+ json: (Optional dict) The json to send with the request.
99
+ headers: (Optional dict) The headers to send with the request.
100
+
101
+ Returns:
102
+ Any: The response from the API.
103
+
104
+ Raises:
105
+ HTTPError: If the request fails.
106
+ """
107
+ result: list = list()
108
+
109
+ call = self.call_api(url, method, json, headers, page_size)
110
+ with tqdm(total=call['count']) as pbar:
111
+ while call['next']:
112
+ result.extend(call['results'])
113
+ pbar.update(len(call['results']))
114
+ pbar.refresh()
115
+ call = self.call_api(url, method, json, headers, page_size)
116
+
117
+ result.extend(call['results'])
118
+ pbar.update(len(call['results']))
119
+
120
+ return result
121
+
122
+ def _perform_call(
123
+ self,
124
+ url: str,
125
+ method: str,
126
+ json: Optional[dict],
127
+ headers: Optional[dict],
128
+ page_size: Optional[int],
129
+ ) -> Response:
130
+ """Perform the API call.
131
+
132
+ Args:
133
+ url: (str) The url to call.
134
+ method: (str) The method to use for the request (GET, POST, PUT, DELETE...).
135
+ json: (Optional dict) The json to send with the request.
136
+ headers: (Optional dict) The headers to send with the request.
137
+ page_size: (Optional int) Specify the page size for the pagination. None means no pagination.
138
+
139
+
140
+ Returns:
141
+ Response: The Response object from requests.
142
+
143
+ Raises:
144
+ HTTPError: If the request fails.
145
+ """
146
+ if headers is None:
147
+ headers = {}
148
+
149
+ auth_header = self.auth.get_authentication_header()
150
+ headers = {**headers, **auth_header}
151
+ url = Client._set_page_size(url, page_size)
152
+
153
+ response = requests.request(method, url, headers=headers, json=json, timeout=self.REQUEST_TIMEOUT)
154
+ return response
155
+
156
+ @staticmethod
157
+ def _set_page_size(url: str, page_size: Optional[int]) -> str:
158
+ """Append the pagination to the url.
159
+
160
+ By default, the API paginates the results. To get all the results, we need to set the pageSize to None.
161
+
162
+ Args:
163
+ url: The url to append the pagination to.
164
+
165
+ Returns:
166
+ str: The url with the pagination appended.
167
+ """
168
+ if 'pageSize' in url:
169
+ return url
170
+
171
+ if '?' in url:
172
+ url += '&'
173
+ else:
174
+ url += '?'
175
+ url += f'pageSize={page_size}'
176
+ return url
@@ -0,0 +1,56 @@
1
+ """Wrap all the result of the client."""
2
+ from typing import Protocol
3
+
4
+ import pandas as pd
5
+
6
+
7
+ class Result(Protocol):
8
+ """Wrap the result of the client."""
9
+
10
+ def to_pandas(self) -> pd.DataFrame:
11
+ """Return the result as a pandas DataFrame."""
12
+ ...
13
+
14
+ def to_json(self) -> dict | list:
15
+ """Return the result as a dict."""
16
+ ...
17
+
18
+
19
+ class DictResult:
20
+ """Wrap dict result."""
21
+
22
+ def __init__(self, data: dict | list) -> None:
23
+ """Generate the result class.
24
+
25
+ Args:
26
+ data (dict): data from the api
27
+ """
28
+ self._data = data
29
+
30
+ def to_pandas(self) -> pd.DataFrame:
31
+ """Return the result as a pandas DataFrame."""
32
+ return pd.DataFrame(self._data)
33
+
34
+ def to_json(self) -> dict | list:
35
+ """Return the result as a dict."""
36
+ return self._data
37
+
38
+
39
+ class PandasResult:
40
+ """Wrap Pandas result."""
41
+
42
+ def __init__(self, data: pd.DataFrame) -> None:
43
+ """Generate the result class.
44
+
45
+ Args:
46
+ data (dict): data from the api
47
+ """
48
+ self._data = data
49
+
50
+ def to_pandas(self) -> pd.DataFrame:
51
+ """Return the result as a pandas DataFrame."""
52
+ return self._data
53
+
54
+ def to_json(self) -> dict | list:
55
+ """Return the result as a dict."""
56
+ return self._data.to_dict('records')
@@ -0,0 +1,11 @@
1
+ """Constants for the project."""
2
+ import os
3
+
4
+ ALLPHINS_API_URL = f"{os.environ.get('ALLPHINS_URL', 'https://app.allphins.com')}/api/v1"
5
+ ACCESS_TOKEN_URL = f'{ALLPHINS_API_URL}/token/'
6
+ REFRESH_TOKEN_URL = f'{ALLPHINS_API_URL}/token/refresh/'
7
+
8
+ GET = 'GET'
9
+ POST = 'POST'
10
+
11
+ ISO_8601 = '%Y-%m-%d'
@@ -0,0 +1,115 @@
1
+ """This module provides methods to interact with the allphins API."""
2
+ import logging
3
+ import uuid
4
+ from typing import Optional
5
+
6
+ from pandas import DataFrame
7
+
8
+ from allphins.models import Policy
9
+ from allphins.models import Portfolio
10
+ from allphins.models import Risk
11
+ from allphins.models.policy import PolicyStatuses
12
+ from allphins.utils import validate_uuid4
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_portfolios() -> DataFrame:
18
+ """Get all the portfolios.
19
+
20
+ Returns:
21
+ dataframe of the portfolios' representation.
22
+ """
23
+ return Portfolio.all().to_pandas()
24
+
25
+
26
+ def get_policies(
27
+ portfolio_id: Optional[uuid.UUID] = None,
28
+ status: Optional[str] = 'written',
29
+ filter_rule: Optional[str] = 'today',
30
+ ) -> DataFrame:
31
+ """Get the policies, using filtering parameters.
32
+
33
+ Values for status:
34
+ * quote
35
+ * written
36
+ * expired
37
+ * declined
38
+ * not_taken_up
39
+ * work_in_progress
40
+ * deleted
41
+
42
+ Values for filter_rule:
43
+ * today (policies live today)
44
+ * previous_1_1 (policies live on the previous 1st of January)
45
+
46
+ Args:
47
+ portfolio_id (Optional[uuid.UUID]): UUID of the portfolio.
48
+ status (Optional[str]): Status of the policy
49
+ filter_rule (Optional[str]): Filter rule to apply
50
+
51
+ Returns:
52
+ dataframe of the policies' representation.
53
+ """
54
+ filters: dict = {}
55
+
56
+ if portfolio_id:
57
+ if not validate_uuid4(portfolio_id):
58
+ raise ValueError(f'{portfolio_id} is not a valid UUID.')
59
+ filters['portfolio_id'] = portfolio_id
60
+
61
+ if filter_rule:
62
+ if filter_rule not in ['today', 'previous_1_1']:
63
+ raise ValueError(f'{filter_rule} is not a valid filter rule.')
64
+ filters['filter_rule'] = filter_rule
65
+
66
+ if status:
67
+ try:
68
+ PolicyStatuses(status)
69
+ except ValueError:
70
+ raise ValueError(f'{status} is not a valid status.')
71
+ filters['status'] = status
72
+
73
+ return Policy.filtered_policies(filters).to_pandas()
74
+
75
+
76
+ def get_risks(
77
+ portfolio_id: Optional[uuid.UUID] = None,
78
+ datasource_id: Optional[uuid.UUID] = None,
79
+ scenario_id: Optional[int] = None,
80
+ ) -> DataFrame:
81
+ """Get the risks, using filtering parameters.
82
+
83
+ At least one of the parameters must be provided.
84
+
85
+ Fetching the risks from the API could take a while, depending on the amount of data to retrieve.
86
+
87
+ Args:
88
+ portfolio_id (Optional[uuid.UUID]): UUID of the portfolio.
89
+ datasource_id (Optional[uuid.UUID]): UUID of the datasource_id.
90
+ scenario_id (Optional[int]): id of the scenario_id.
91
+
92
+ Returns:
93
+ dataframe of the risks' representation.
94
+ """
95
+ if not any([portfolio_id, datasource_id, scenario_id]):
96
+ raise ValueError('At least one of the parameters must be provided.')
97
+
98
+ filters: dict = {}
99
+
100
+ if portfolio_id:
101
+ if not validate_uuid4(portfolio_id):
102
+ raise ValueError(f'{portfolio_id} is not a valid UUID.')
103
+ filters['portfolios'] = portfolio_id
104
+
105
+ if datasource_id:
106
+ if not validate_uuid4(datasource_id):
107
+ raise ValueError(f'{datasource_id} is not a valid UUID.')
108
+ filters['datasource_id'] = datasource_id
109
+
110
+ if scenario_id:
111
+ filters['scenario_id'] = scenario_id
112
+
113
+ logger.info('Fetching risks from the API, this could take a while...')
114
+
115
+ return Risk.filtered_risk(filters).to_pandas()
@@ -0,0 +1,10 @@
1
+ """Get resources from Allphins API."""
2
+
3
+ from allphins.models.datasource import Datasource
4
+ from allphins.models.policy import Policy
5
+ from allphins.models.portfolio import Portfolio
6
+ from allphins.models.risk import Risk
7
+ from allphins.models.scenario import Scenario
8
+ from allphins.models.scenario_list import ScenarioList
9
+
10
+ __all__ = ["Datasource", "Policy", "Portfolio", "Risk", "Scenario", "ScenarioList"]
@@ -0,0 +1,75 @@
1
+ """Base class for models."""
2
+ from typing import Any
3
+ from typing import TypeVar
4
+
5
+ from allphins.client.client import Client
6
+ from allphins.client.result import DictResult
7
+ from allphins.client.result import Result
8
+ from allphins.const import GET
9
+
10
+ T = TypeVar('T', bound='BaseModel')
11
+
12
+
13
+ class BaseModel:
14
+ """This class is the base for all the models.
15
+
16
+ It serves as a wrapper for all operations (API calls) that can be done on the model.
17
+ All class have a method to_dict that returns the data of the model as a dict through a GET query to the API.
18
+
19
+ Attributes:
20
+ _cache: basic cache for the API calls based on the url called
21
+ _client: The client to use for API calls.
22
+ _model_id: The id of the model.
23
+ """
24
+
25
+ path: str = None
26
+
27
+ def __init__(self, model_id: str = None):
28
+ """Initialize the model."""
29
+ self._cache: dict = {}
30
+ self._client = Client()
31
+ self._model_id = model_id
32
+
33
+ @property
34
+ def _item_path(self) -> str:
35
+ """Get the path of the model.
36
+
37
+ Returns:
38
+ str: The path of the model.
39
+ """
40
+ return f'{self.path}{self._model_id}/'
41
+
42
+ @classmethod
43
+ def from_id(cls: type[T], model_id: str) -> T:
44
+ """Instantiate a Model from its id.
45
+
46
+ Args:
47
+ model_id (str): id of the model.
48
+
49
+ Returns:
50
+ BaseModel: Instance of the model.
51
+ """
52
+ return cls(model_id)
53
+
54
+ def get_item(self) -> Result:
55
+ """Get the data of the model in a dict format.
56
+
57
+ Returns:
58
+ dict: Dict containing the response of the API call.
59
+ """
60
+ return DictResult(self._get(self._item_path))
61
+
62
+ def _get(self, url: str) -> Any:
63
+ """Perform a GET request on the API.
64
+
65
+ Use a basic cache to avoid calling the API multiple times for the same url.
66
+
67
+ Args:
68
+ url (str): The url to call.
69
+
70
+ Returns:
71
+ Any: The response of the API call.
72
+ """
73
+ if url not in self._cache:
74
+ self._cache[url] = self._client.call_api(url, GET)
75
+ return self._cache[url]
@@ -0,0 +1,61 @@
1
+ """Datasource model.
2
+
3
+ ```
4
+ from allphins.models import Datasource
5
+ ```
6
+ """
7
+ import pandas as pd
8
+
9
+ from allphins.client.result import PandasResult
10
+ from allphins.client.result import Result
11
+ from allphins.const import ALLPHINS_API_URL
12
+ from allphins.models.base import BaseModel
13
+ from allphins.utils import validate_uuid4
14
+
15
+
16
+ class Datasource(BaseModel):
17
+ """Datasource model."""
18
+
19
+ path = f'{ALLPHINS_API_URL}/datasources/'
20
+
21
+ def __init__(self, datasource_id: str):
22
+ """Instantiate a Datasource from a datasource UUID.
23
+
24
+ Args:
25
+ datasource_id (str): UUID of the datasource.
26
+
27
+ Raises:
28
+ ValueError: If the datasource_id is not a valid UUID.
29
+ """
30
+ if not validate_uuid4(datasource_id):
31
+ raise ValueError(f'{datasource_id} is not a valid UUID.')
32
+
33
+ super().__init__(datasource_id)
34
+
35
+ @classmethod
36
+ def from_portfolio_id(cls, portfolio_id: str) -> list['Datasource']:
37
+ """Instantiate a list of Datasource from a portfolio UUID.
38
+
39
+ To get the dict representation of the portfolio datasource,
40
+ use the datasources_to_dict() method of the portfolio.
41
+
42
+ Args:
43
+ portfolio_id (str): UUID of the portfolio.
44
+
45
+ Returns:
46
+ list[Datasource]: List of datasource.
47
+ """
48
+ from allphins.models.portfolio import Portfolio
49
+
50
+ return Portfolio.from_id(portfolio_id).datasource_models
51
+
52
+ def parquet_risks(self) -> Result:
53
+ """Get the parquet risks of the datasource as a pandas DataFrame.
54
+
55
+ Returns:
56
+ DataFrame: Pandas DataFrame containing the risks.
57
+ """
58
+ response = self._get(f'{self.path}{self._model_id}/parquet_signed_urls/?type=RISKS')
59
+ risks_urls = response.get('results', [])
60
+ risks_table = pd.concat(pd.read_parquet(url) for url in risks_urls)
61
+ return PandasResult(risks_table)
@@ -0,0 +1,100 @@
1
+ """Policy model.
2
+
3
+ ```
4
+ from allphins.models import Policy
5
+ ```
6
+ """
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from typing import Optional
10
+
11
+ from allphins.client.client import Client
12
+ from allphins.client.result import DictResult
13
+ from allphins.client.result import Result
14
+ from allphins.const import ALLPHINS_API_URL
15
+ from allphins.const import GET
16
+ from allphins.const import ISO_8601
17
+ from allphins.models.base import BaseModel
18
+
19
+
20
+ class PolicyStatuses(str, Enum):
21
+ """Policy status choices."""
22
+
23
+ QUOTE = 'quote'
24
+ WRITTEN = 'written'
25
+ EXPIRED = 'expired'
26
+ DECLINED = 'declined'
27
+ NOT_TAKEN_UP = 'not_taken_up'
28
+ WORK_IN_PROGRESS = 'work_in_progress'
29
+ DELETED = 'deleted'
30
+
31
+
32
+ class Policy(BaseModel):
33
+ """Policy model."""
34
+
35
+ path = f'{ALLPHINS_API_URL}/policies/'
36
+
37
+ @classmethod
38
+ def live_policies(cls, policy_live_at: Optional[str] = None, filter_rule: Optional[str] = None) -> Result:
39
+ """Get the live policies matching the policy_live_at date or the filter_rule.
40
+
41
+ policy_live_at and filter_rule are mutually exclusive, but at least one of them must be provided.
42
+
43
+ Args:
44
+ policy_live_at (Optional str): Date of the policy live.
45
+ filter_rule (Optional str): Filter rule to apply ('today', 'previous_1_1', 'next_1_1').
46
+
47
+ Returns:
48
+ list[dict]: List of policies on dict format.
49
+ """
50
+ url = ""
51
+ if policy_live_at and filter_rule:
52
+ raise ValueError('policy_live_at and filter_rule are mutually exclusive.')
53
+
54
+ if not policy_live_at and not filter_rule:
55
+ raise ValueError('policy_live_at or filter_rule must be provided.')
56
+
57
+ if policy_live_at:
58
+ try:
59
+ datetime.strptime(policy_live_at, ISO_8601).date()
60
+ url = f'{cls.path}?policy_live_at={policy_live_at}'
61
+ except ValueError:
62
+ raise ValueError(f'{policy_live_at} is not a valid ISO 8601 date.')
63
+
64
+ if filter_rule:
65
+ if filter_rule not in ['today', 'previous_1_1', 'next_1_1']:
66
+ raise ValueError(f'{filter_rule} is not a valid filter rule.')
67
+ url = f'{cls.path}?filter_rule={filter_rule}'
68
+
69
+ return DictResult(Client().call_api(url, GET))
70
+
71
+ @classmethod
72
+ def from_portfolio_id(cls, portfolio_id: str) -> list['Policy']:
73
+ """Instantiate a list of policy from a portfolio UUID.
74
+
75
+ To get the dict representation of the portfolio policies, use the policies_to_dict() method of the portfolio.
76
+
77
+ Args:
78
+ portfolio_id (str): UUID of the portfolio.
79
+
80
+ Returns:
81
+ list[Policy]: List of policy instances.
82
+ """
83
+ from allphins.models.portfolio import Portfolio
84
+
85
+ return Portfolio.from_id(portfolio_id).policy_models
86
+
87
+ @classmethod
88
+ def filtered_policies(cls, filters: dict) -> Result:
89
+ """Get the policy ids matching the filters.
90
+
91
+ Args:
92
+ filters (dict): Filters to apply.
93
+
94
+ Returns:
95
+ Result: List of policy ids.
96
+ """
97
+ url = f'{cls.path}filter/?details=true&'
98
+ for key, value in filters.items():
99
+ url += f'{key}={value}&'
100
+ return DictResult(Client().call_api(url[:-1], GET, append_none_pagination=False))
@@ -0,0 +1,90 @@
1
+ """Portfolio model.
2
+
3
+ ```
4
+ from allphins.models import Portfolio
5
+ ```
6
+ """
7
+
8
+ from allphins.client.client import Client
9
+ from allphins.client.result import DictResult
10
+ from allphins.client.result import Result
11
+ from allphins.const import ALLPHINS_API_URL
12
+ from allphins.const import GET
13
+ from allphins.models.base import BaseModel
14
+ from allphins.models.datasource import Datasource
15
+ from allphins.models.policy import Policy
16
+ from allphins.utils import validate_uuid4
17
+
18
+
19
+ class Portfolio(BaseModel):
20
+ """Portfolio model."""
21
+
22
+ path = f'{ALLPHINS_API_URL}/portfolios/'
23
+
24
+ def __init__(self, portfolio_id: str):
25
+ """Instantiate a Portfolio from a portfolio UUID.
26
+
27
+ Args:
28
+ portfolio_id (str): UUID of the portfolio.
29
+
30
+ Raises:
31
+ ValueError: If the portfolio_id is not a valid UUID.
32
+ """
33
+ if not validate_uuid4(portfolio_id):
34
+ raise ValueError(f'{portfolio_id} is not a valid UUID.')
35
+
36
+ super().__init__(portfolio_id)
37
+
38
+ @classmethod
39
+ def all(cls) -> Result:
40
+ """Get all the portfolios.
41
+
42
+ Returns:
43
+ list[dict]: List of the portfolios dict representation.
44
+ """
45
+ return DictResult(Client().call_api(Portfolio.path, GET))
46
+
47
+ @property
48
+ def policy_models(self) -> list[Policy]:
49
+ """Get the Policy list of the Portfolio.
50
+
51
+ To get the data of the policies as a dict, use the policies_to_dict() method.
52
+
53
+ Returns:
54
+ list[Policy]: List of Policy instances.
55
+ """
56
+ item = self.get_item().to_json()
57
+ if isinstance(item, dict):
58
+ return [Policy.from_id(policy['id']) for policy in item['policies']]
59
+ raise TypeError('The portfolio is not a dict.')
60
+
61
+ @property
62
+ def datasource_models(self) -> list[Datasource]:
63
+ """Get the Datasource list of the Portfolio.
64
+
65
+ To get the data of the datasources as a dict, use the datasources_to_dict() method.
66
+
67
+ Returns:
68
+ list[Datasource]: List of Datasource instances.
69
+ """
70
+ datasources_details = self._get(f'{self._item_path}datasources/')
71
+ return [Datasource.from_id(datasource['id']) for datasource in datasources_details]
72
+
73
+ def policies(self) -> Result:
74
+ """Get the policies of the portfolio.
75
+
76
+ Returns:
77
+ dict: Dict representation of the policies.
78
+ """
79
+ item = self.get_item().to_json()
80
+ if isinstance(item, dict):
81
+ return DictResult(item['policies'])
82
+ raise TypeError('The portfolio is not a dict.')
83
+
84
+ def datasources(self) -> Result:
85
+ """Get the datasources of the portfolio.
86
+
87
+ Returns:
88
+ dict: Dict representation of the datasources.
89
+ """
90
+ return DictResult(self._get(f'{self._item_path}datasources/'))
@@ -0,0 +1,27 @@
1
+ """Risk model."""
2
+ from allphins.client import Client
3
+ from allphins.client.result import DictResult
4
+ from allphins.client.result import Result
5
+ from allphins.const import ALLPHINS_API_URL
6
+ from allphins.const import POST
7
+ from allphins.models.base import BaseModel
8
+
9
+
10
+ class Risk(BaseModel):
11
+ """Risk model."""
12
+
13
+ path = f'{ALLPHINS_API_URL}/risks/list_details/'
14
+ PAGE_SIZE = 50000
15
+
16
+ @classmethod
17
+ def filtered_risk(cls, filters: dict) -> Result:
18
+ """Get the filtered risks.
19
+
20
+ Args:
21
+ filters (dict): Filters to apply.
22
+
23
+ Returns:
24
+ Risk: Risk object.
25
+ """
26
+ risks = Client().call_api_with_pagination(cls.path, POST, json=filters, page_size=Risk.PAGE_SIZE)
27
+ return DictResult(risks)
@@ -0,0 +1,15 @@
1
+ """Scenario model.
2
+
3
+ ```
4
+ from allphins.models import Scenario
5
+ ```
6
+ """
7
+
8
+ from allphins.const import ALLPHINS_API_URL
9
+ from allphins.models.base import BaseModel
10
+
11
+
12
+ class Scenario(BaseModel):
13
+ """Scenario model."""
14
+
15
+ path = f'{ALLPHINS_API_URL}/scenarios/'
@@ -0,0 +1,31 @@
1
+ """ScenarioList model.
2
+
3
+ ```
4
+ from allphins.models import ScenarioList
5
+ ```
6
+ """
7
+ from typing import Optional
8
+
9
+ from allphins.const import ALLPHINS_API_URL
10
+ from allphins.models.base import BaseModel
11
+ from allphins.utils import validate_uuid4
12
+
13
+
14
+ class ScenarioList(BaseModel):
15
+ """ScenarioList model."""
16
+
17
+ path = f'{ALLPHINS_API_URL}/scenario_lists/'
18
+
19
+ def __init__(self, scenario_list_id: Optional[str]):
20
+ """Instantiate a Scenario list from a scenario list UUID.
21
+
22
+ Args:
23
+ scenario_list_id (str): UUID of the scenario list.
24
+
25
+ Raises:
26
+ ValueError: If the scenario list_id is not a valid UUID.
27
+ """
28
+ if not validate_uuid4(scenario_list_id):
29
+ raise ValueError(f'{scenario_list_id} is not a valid UUID.')
30
+
31
+ super().__init__(scenario_list_id)
@@ -0,0 +1,13 @@
1
+ """Singleton metaclass."""
2
+
3
+
4
+ class Singleton(type):
5
+ """Singleton metaclass."""
6
+
7
+ _instances: dict = {}
8
+
9
+ def __call__(cls, *args, **kwargs):
10
+ """Call the class."""
11
+ if cls not in cls._instances:
12
+ cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
13
+ return cls._instances[cls]
@@ -0,0 +1,72 @@
1
+ """
2
+ Descriptive HTTP status codes, for code readability.
3
+
4
+ See RFC 2616 - https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
5
+ And RFC 6585 - https://tools.ietf.org/html/rfc6585
6
+ And RFC 4918 - https://tools.ietf.org/html/rfc4918
7
+ """
8
+
9
+ HTTP_100_CONTINUE = 100
10
+ HTTP_101_SWITCHING_PROTOCOLS = 101
11
+ HTTP_102_PROCESSING = 102
12
+ HTTP_103_EARLY_HINTS = 103
13
+ HTTP_200_OK = 200
14
+ HTTP_201_CREATED = 201
15
+ HTTP_202_ACCEPTED = 202
16
+ HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
17
+ HTTP_204_NO_CONTENT = 204
18
+ HTTP_205_RESET_CONTENT = 205
19
+ HTTP_206_PARTIAL_CONTENT = 206
20
+ HTTP_207_MULTI_STATUS = 207
21
+ HTTP_208_ALREADY_REPORTED = 208
22
+ HTTP_226_IM_USED = 226
23
+ HTTP_300_MULTIPLE_CHOICES = 300
24
+ HTTP_301_MOVED_PERMANENTLY = 301
25
+ HTTP_302_FOUND = 302
26
+ HTTP_303_SEE_OTHER = 303
27
+ HTTP_304_NOT_MODIFIED = 304
28
+ HTTP_305_USE_PROXY = 305
29
+ HTTP_306_RESERVED = 306
30
+ HTTP_307_TEMPORARY_REDIRECT = 307
31
+ HTTP_308_PERMANENT_REDIRECT = 308
32
+ HTTP_400_BAD_REQUEST = 400
33
+ HTTP_401_UNAUTHORIZED = 401
34
+ HTTP_402_PAYMENT_REQUIRED = 402
35
+ HTTP_403_FORBIDDEN = 403
36
+ HTTP_404_NOT_FOUND = 404
37
+ HTTP_405_METHOD_NOT_ALLOWED = 405
38
+ HTTP_406_NOT_ACCEPTABLE = 406
39
+ HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
40
+ HTTP_408_REQUEST_TIMEOUT = 408
41
+ HTTP_409_CONFLICT = 409
42
+ HTTP_410_GONE = 410
43
+ HTTP_411_LENGTH_REQUIRED = 411
44
+ HTTP_412_PRECONDITION_FAILED = 412
45
+ HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
46
+ HTTP_414_REQUEST_URI_TOO_LONG = 414
47
+ HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
48
+ HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
49
+ HTTP_417_EXPECTATION_FAILED = 417
50
+ HTTP_418_IM_A_TEAPOT = 418
51
+ HTTP_421_MISDIRECTED_REQUEST = 421
52
+ HTTP_422_UNPROCESSABLE_ENTITY = 422
53
+ HTTP_423_LOCKED = 423
54
+ HTTP_424_FAILED_DEPENDENCY = 424
55
+ HTTP_425_TOO_EARLY = 425
56
+ HTTP_426_UPGRADE_REQUIRED = 426
57
+ HTTP_428_PRECONDITION_REQUIRED = 428
58
+ HTTP_429_TOO_MANY_REQUESTS = 429
59
+ HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
60
+ HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
61
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
62
+ HTTP_501_NOT_IMPLEMENTED = 501
63
+ HTTP_502_BAD_GATEWAY = 502
64
+ HTTP_503_SERVICE_UNAVAILABLE = 503
65
+ HTTP_504_GATEWAY_TIMEOUT = 504
66
+ HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
67
+ HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
68
+ HTTP_507_INSUFFICIENT_STORAGE = 507
69
+ HTTP_508_LOOP_DETECTED = 508
70
+ HTTP_509_BANDWIDTH_LIMIT_EXCEEDED = 509
71
+ HTTP_510_NOT_EXTENDED = 510
72
+ HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
@@ -0,0 +1,25 @@
1
+ import uuid
2
+ from uuid import UUID
3
+
4
+
5
+ def validate_uuid4(uuid_string: UUID | str) -> bool:
6
+ """Validate that a UUID string is in fact a valid uuid4.
7
+
8
+ Args
9
+ uuid_string: The string to validate.
10
+
11
+ Returns:
12
+ True if uuid_string is a valid uuid4, False otherwise.
13
+ """
14
+ if uuid_string is None:
15
+ return False
16
+
17
+ if isinstance(uuid_string, uuid.UUID):
18
+ return True
19
+
20
+ try:
21
+ val = UUID(uuid_string, version=4)
22
+ except ValueError:
23
+ return False
24
+
25
+ return val.hex == uuid_string.replace('-', '')
@@ -0,0 +1,92 @@
1
+ [tool.isort]
2
+ line_length = 120
3
+ profile = "black"
4
+ ensure_newline_before_comments = true
5
+ force_single_line = true
6
+
7
+ [tool.black]
8
+ line-length = 120
9
+ target-version = ["py310"]
10
+ skip-string-normalization = true
11
+ force-exclude = '''
12
+ /(
13
+ migrations
14
+
15
+ # black defaults
16
+ | \.git
17
+ | \.hg
18
+ | \.mypy_cache
19
+ | \.tox
20
+ | \.venv
21
+ | _build
22
+ | buck-out
23
+ | build
24
+ | dist
25
+ )/
26
+ '''
27
+
28
+ [tool.bandit]
29
+ exclude_dirs = ['tests']
30
+
31
+ [tool.mypy]
32
+ python_version = "3.10"
33
+ pretty = true
34
+ show_column_numbers = true
35
+ show_error_codes = true
36
+ check_untyped_defs = true
37
+ warn_unused_ignores = true
38
+ enable_error_code = "ignore-without-code"
39
+ namespace_packages = true
40
+ follow_imports = "skip"
41
+
42
+ [[tool.mypy.overrides]]
43
+ module = [
44
+ "dateparser.*",
45
+ "dateutil.*",
46
+ "six.*",
47
+ "requests.*",
48
+ ]
49
+ ignore_missing_imports = true
50
+
51
+ [tool.flake8]
52
+ max-line-length = 120
53
+ ban-relative-imports = true
54
+ select = ["B", "I", "E", "W", "F", "INP"]
55
+ ignore = ["E501", "E203", "W503"]
56
+
57
+ [tool.pydocstyle]
58
+ convention = "google"
59
+
60
+ [tool.coverage.run]
61
+ disable_warnings = ["no-data-collected"]
62
+
63
+ [tool.poetry]
64
+ name = "allphins-sdk"
65
+ version = "1.0.4"
66
+ description = ""
67
+ authors = ["Florent <florent@allphins.com>"]
68
+ readme = "README.md"
69
+ packages = [{include = "allphins"}]
70
+
71
+ [tool.poetry.dependencies]
72
+ python = "^3.10"
73
+ requests = "^2.31.0"
74
+ pandas = "^2.1.4"
75
+ pyarrow = "^15.0.0"
76
+ tqdm = "^4.66.2"
77
+
78
+ [tool.poetry.group.dev]
79
+ optional = true
80
+
81
+ [tool.poetry.group.dev.dependencies]
82
+ flake8 = "^6.1.0"
83
+ coverage = {extras = ["toml"], version = "^7.3.2"}
84
+ pydocstyle = "^6.3.0"
85
+ mkdocs = "^1.5.3"
86
+ mkdocs-material = "^9.5.2"
87
+ mkdocstrings = {extras = ["python"], version = "^0.24.0"}
88
+ jupyter = "^1.0.0"
89
+
90
+ [build-system]
91
+ requires = ["poetry-core"]
92
+ build-backend = "poetry.core.masonry.api"