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.
- allphins_sdk-1.0.4/PKG-INFO +67 -0
- allphins_sdk-1.0.4/README.md +50 -0
- allphins_sdk-1.0.4/allphins/__init__.py +14 -0
- allphins_sdk-1.0.4/allphins/client/__init__.py +5 -0
- allphins_sdk-1.0.4/allphins/client/auth.py +101 -0
- allphins_sdk-1.0.4/allphins/client/client.py +176 -0
- allphins_sdk-1.0.4/allphins/client/result.py +56 -0
- allphins_sdk-1.0.4/allphins/const.py +11 -0
- allphins_sdk-1.0.4/allphins/interface.py +115 -0
- allphins_sdk-1.0.4/allphins/models/__init__.py +10 -0
- allphins_sdk-1.0.4/allphins/models/base.py +75 -0
- allphins_sdk-1.0.4/allphins/models/datasource.py +61 -0
- allphins_sdk-1.0.4/allphins/models/policy.py +100 -0
- allphins_sdk-1.0.4/allphins/models/portfolio.py +90 -0
- allphins_sdk-1.0.4/allphins/models/risk.py +27 -0
- allphins_sdk-1.0.4/allphins/models/scenario.py +15 -0
- allphins_sdk-1.0.4/allphins/models/scenario_list.py +31 -0
- allphins_sdk-1.0.4/allphins/singleton.py +13 -0
- allphins_sdk-1.0.4/allphins/status.py +72 -0
- allphins_sdk-1.0.4/allphins/utils.py +25 -0
- allphins_sdk-1.0.4/pyproject.toml +92 -0
|
@@ -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,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"
|