vhi-python 0.1.0__py3-none-any.whl
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.
- vhi/__init__.py +11 -0
- vhi/client.py +199 -0
- vhi/exceptions.py +18 -0
- vhi/models.py +20 -0
- vhi_python-0.1.0.dist-info/METADATA +110 -0
- vhi_python-0.1.0.dist-info/RECORD +8 -0
- vhi_python-0.1.0.dist-info/WHEEL +5 -0
- vhi_python-0.1.0.dist-info/top_level.txt +1 -0
vhi/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .client import VhiClient
|
|
2
|
+
from .exceptions import VhiAuthenticationError, VhiMfaRequiredError, VhiApiError
|
|
3
|
+
from .models import ClaimStatement
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"VhiClient",
|
|
7
|
+
"VhiAuthenticationError",
|
|
8
|
+
"VhiMfaRequiredError",
|
|
9
|
+
"VhiApiError",
|
|
10
|
+
"ClaimStatement"
|
|
11
|
+
]
|
vhi/client.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import os
|
|
3
|
+
from typing import Callable, Optional, List
|
|
4
|
+
from .exceptions import VhiAuthenticationError, VhiMfaRequiredError, VhiApiError
|
|
5
|
+
from .models import ClaimStatement
|
|
6
|
+
|
|
7
|
+
class VhiClient:
|
|
8
|
+
"""
|
|
9
|
+
A client library for interacting with the Vhi API.
|
|
10
|
+
Handles authentication, MFA callbacks, claims retrieval, and document downloads.
|
|
11
|
+
"""
|
|
12
|
+
DEFAULT_CONFIG_URL = "https://www.vhi.ie/myvhi/myclaimstatements" # Usually embedded here
|
|
13
|
+
|
|
14
|
+
def __init__(self, username: str, password: str, mfa_callback: Optional[Callable[[], str]] = None):
|
|
15
|
+
"""
|
|
16
|
+
Initialize the VhiClient.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
username: User's email address.
|
|
20
|
+
password: User's password.
|
|
21
|
+
mfa_callback: A callable that returns the 2FA OTP string when invoked.
|
|
22
|
+
"""
|
|
23
|
+
self.username = username
|
|
24
|
+
self.password = password
|
|
25
|
+
self.mfa_callback = mfa_callback
|
|
26
|
+
|
|
27
|
+
# State management
|
|
28
|
+
self.session = requests.Session()
|
|
29
|
+
|
|
30
|
+
# Default Browser Headers to mimic browser and avoid WAF
|
|
31
|
+
self.session.headers.update({
|
|
32
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
33
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
self.is_authenticated = False
|
|
37
|
+
self.apis_base_url = "https://apis.vhi.ie" # Will be updated during discovery if necessary
|
|
38
|
+
|
|
39
|
+
def _fetch_environment_config(self):
|
|
40
|
+
"""
|
|
41
|
+
Optional: Dynamically discovers the API base URL.
|
|
42
|
+
Currently defaults to known https://apis.vhi.ie.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
# We skip full parsing of the config to avoid complex scraping
|
|
46
|
+
# We hardcode the reliable apis.vhi.ie gateway for now as per instructions.
|
|
47
|
+
pass
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def login(self):
|
|
52
|
+
"""
|
|
53
|
+
Performs the login flow. Handles the MFA challenge if required.
|
|
54
|
+
"""
|
|
55
|
+
# Set proper fetch headers
|
|
56
|
+
headers = {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
"Accept": "application/json",
|
|
59
|
+
"Origin": "https://www.vhi.ie",
|
|
60
|
+
"Referer": "https://www.vhi.ie/",
|
|
61
|
+
"Sec-Fetch-Mode": "cors",
|
|
62
|
+
"Sec-Fetch-Site": "same-site",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
payload = {
|
|
66
|
+
"username": self.username,
|
|
67
|
+
"password": self.password
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# 1. Primary Authentication
|
|
71
|
+
# The path here is a presumed standard, will likely be updated if the actual path varies
|
|
72
|
+
login_url = f"{self.apis_base_url}/auth/v1/login"
|
|
73
|
+
|
|
74
|
+
response = self.session.post(login_url, json=payload, headers=headers)
|
|
75
|
+
|
|
76
|
+
if response.status_code == 200:
|
|
77
|
+
self.is_authenticated = True
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
elif response.status_code == 202:
|
|
81
|
+
# MFA Challenge Required
|
|
82
|
+
try:
|
|
83
|
+
data = response.json()
|
|
84
|
+
except ValueError:
|
|
85
|
+
raise VhiApiError("MFA response was not valid JSON", status_code=response.status_code)
|
|
86
|
+
|
|
87
|
+
if data.get("mfa_required"):
|
|
88
|
+
state_token = data.get("state_token")
|
|
89
|
+
if not state_token:
|
|
90
|
+
raise VhiApiError("MFA required but no state_token provided in response")
|
|
91
|
+
|
|
92
|
+
self._handle_mfa_challenge(state_token, headers)
|
|
93
|
+
else:
|
|
94
|
+
raise VhiApiError("Received 202 but not MFA challenge", status_code=response.status_code, response=response)
|
|
95
|
+
|
|
96
|
+
elif response.status_code == 401:
|
|
97
|
+
raise VhiAuthenticationError("Invalid credentials provided.")
|
|
98
|
+
else:
|
|
99
|
+
raise VhiApiError(f"Login failed with status {response.status_code}", status_code=response.status_code, response=response)
|
|
100
|
+
|
|
101
|
+
def _handle_mfa_challenge(self, state_token: str, base_headers: dict):
|
|
102
|
+
"""
|
|
103
|
+
Internal method to submit the MFA code.
|
|
104
|
+
"""
|
|
105
|
+
if not self.mfa_callback:
|
|
106
|
+
raise VhiMfaRequiredError("MFA is required but no mfa_callback was provided to VhiClient.")
|
|
107
|
+
|
|
108
|
+
# Pause execution and invoke the user-defined callback to get the OTP
|
|
109
|
+
otp_code = self.mfa_callback()
|
|
110
|
+
|
|
111
|
+
if not otp_code:
|
|
112
|
+
raise VhiAuthenticationError("MFA callback did not return a valid OTP code.")
|
|
113
|
+
|
|
114
|
+
mfa_url = f"{self.apis_base_url}/auth/v1/mfa/verify"
|
|
115
|
+
payload = {
|
|
116
|
+
"otp": otp_code,
|
|
117
|
+
"state_token": state_token
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
response = self.session.post(mfa_url, json=payload, headers=base_headers)
|
|
121
|
+
|
|
122
|
+
if response.status_code == 200:
|
|
123
|
+
self.is_authenticated = True
|
|
124
|
+
|
|
125
|
+
# The session cookie jar has now been populated with the authorized session
|
|
126
|
+
# Or we might need to extract a Bearer token. This implementation relies
|
|
127
|
+
# on cookies being correctly set via Set-Cookie headers by the API.
|
|
128
|
+
else:
|
|
129
|
+
raise VhiAuthenticationError(f"MFA verification failed. Status: {response.status_code}")
|
|
130
|
+
|
|
131
|
+
def get_claims(self) -> List[ClaimStatement]:
|
|
132
|
+
"""
|
|
133
|
+
Retrieves the list of claim statements.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of ClaimStatement objects.
|
|
137
|
+
"""
|
|
138
|
+
if not self.is_authenticated:
|
|
139
|
+
raise VhiAuthenticationError("Client is not authenticated. Call login() first.")
|
|
140
|
+
|
|
141
|
+
url = f"{self.apis_base_url}/claims/v1/statements"
|
|
142
|
+
|
|
143
|
+
headers = {
|
|
144
|
+
"Accept": "application/json",
|
|
145
|
+
"Origin": "https://www.vhi.ie",
|
|
146
|
+
"Referer": "https://www.vhi.ie/",
|
|
147
|
+
"Sec-Fetch-Mode": "cors",
|
|
148
|
+
"Sec-Fetch-Site": "same-site",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
response = self.session.get(url, headers=headers)
|
|
152
|
+
|
|
153
|
+
if response.status_code == 200:
|
|
154
|
+
try:
|
|
155
|
+
data = response.json()
|
|
156
|
+
except ValueError:
|
|
157
|
+
raise VhiApiError("Invalid JSON received for claims statements.", status_code=response.status_code)
|
|
158
|
+
|
|
159
|
+
claims_data = data.get("claims", [])
|
|
160
|
+
return [ClaimStatement.from_dict(claim) for claim in claims_data]
|
|
161
|
+
elif response.status_code == 401:
|
|
162
|
+
self.is_authenticated = False
|
|
163
|
+
raise VhiAuthenticationError("Session expired or unauthorized.")
|
|
164
|
+
else:
|
|
165
|
+
raise VhiApiError(f"Failed to fetch claims: {response.status_code}", status_code=response.status_code, response=response)
|
|
166
|
+
|
|
167
|
+
def download_document(self, document_id: str, dest_path: str):
|
|
168
|
+
"""
|
|
169
|
+
Streams a claim PDF document to the local disk.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
document_id: The ID of the document to download.
|
|
173
|
+
dest_path: The file path where the PDF should be saved.
|
|
174
|
+
"""
|
|
175
|
+
if not self.is_authenticated:
|
|
176
|
+
raise VhiAuthenticationError("Client is not authenticated. Call login() first.")
|
|
177
|
+
|
|
178
|
+
url = f"{self.apis_base_url}/documents/v1/{document_id}/download"
|
|
179
|
+
|
|
180
|
+
headers = {
|
|
181
|
+
"Accept": "application/pdf",
|
|
182
|
+
"Origin": "https://www.vhi.ie",
|
|
183
|
+
"Referer": "https://www.vhi.ie/",
|
|
184
|
+
"Sec-Fetch-Mode": "cors",
|
|
185
|
+
"Sec-Fetch-Site": "same-site",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Use stream=True to prevent loading large PDFs into memory
|
|
189
|
+
with self.session.get(url, headers=headers, stream=True) as response:
|
|
190
|
+
if response.status_code == 200:
|
|
191
|
+
with open(dest_path, 'wb') as f:
|
|
192
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
193
|
+
if chunk:
|
|
194
|
+
f.write(chunk)
|
|
195
|
+
elif response.status_code == 401:
|
|
196
|
+
self.is_authenticated = False
|
|
197
|
+
raise VhiAuthenticationError("Session expired or unauthorized while downloading document.")
|
|
198
|
+
else:
|
|
199
|
+
raise VhiApiError(f"Failed to download document {document_id}: {response.status_code}", status_code=response.status_code, response=response)
|
vhi/exceptions.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class VhiError(Exception):
|
|
2
|
+
"""Base exception for Vhi Python Client"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class VhiAuthenticationError(VhiError):
|
|
6
|
+
"""Raised when authentication fails (invalid credentials)"""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class VhiMfaRequiredError(VhiError):
|
|
10
|
+
"""Raised when MFA is required but no callback is provided or callback fails"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class VhiApiError(VhiError):
|
|
14
|
+
"""Raised when the Vhi API returns an error response"""
|
|
15
|
+
def __init__(self, message, status_code=None, response=None):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.response = response
|
vhi/models.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class ClaimStatement:
|
|
6
|
+
claim_id: str
|
|
7
|
+
date_of_service: str
|
|
8
|
+
practitioner: str
|
|
9
|
+
document_id: str
|
|
10
|
+
status: str
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_dict(cls, data: dict) -> "ClaimStatement":
|
|
14
|
+
return cls(
|
|
15
|
+
claim_id=data.get("claimId", ""),
|
|
16
|
+
date_of_service=data.get("dateOfService", ""),
|
|
17
|
+
practitioner=data.get("practitioner", ""),
|
|
18
|
+
document_id=data.get("documentId", ""),
|
|
19
|
+
status=data.get("status", "")
|
|
20
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vhi-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python client library for the Vhi API, handling authentication, MFA callbacks, claims retrieval, and document downloads.
|
|
5
|
+
Author-email: Agent <agent@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/agent/vhi-python
|
|
8
|
+
Project-URL: Repository, https://github.com/agent/vhi-python.git
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.25.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
18
|
+
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# Vhi Python Client
|
|
21
|
+
|
|
22
|
+
A Python client library for interacting dynamically with the Vhi API. Provides automated handling of Session state, Multi-Factor Authentication (MFA) callbacks, Claims Retrieval, and memory-efficient Document Downloads.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
You can install the library directly from source (or via pip once published):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install vhi-python
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Alternatively, to install it locally for development:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/agent/vhi-python.git
|
|
36
|
+
cd vhi-python
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start Example
|
|
41
|
+
|
|
42
|
+
This example demonstrates how to use the library to log in, handle an MFA callback (simulated via CLI input), retrieve claims, and download a claim PDF.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
from vhi.client import VhiClient
|
|
47
|
+
from vhi.exceptions import VhiAuthenticationError, VhiApiError
|
|
48
|
+
|
|
49
|
+
def my_cli_mfa_callback() -> str:
|
|
50
|
+
"""
|
|
51
|
+
A simple callback function that pauses execution
|
|
52
|
+
and waits for the user to input the 2FA code sent to their phone/email.
|
|
53
|
+
"""
|
|
54
|
+
return input("Vhi 2FA Code Required. Enter OTP: ").strip()
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
# 1. Initialize the client
|
|
58
|
+
# Replace with your actual credentials
|
|
59
|
+
username = os.getenv("VHI_USERNAME", "your_email@example.com")
|
|
60
|
+
password = os.getenv("VHI_PASSWORD", "your_password")
|
|
61
|
+
|
|
62
|
+
# We pass our callback function so the client can automatically
|
|
63
|
+
# pause and invoke it if the server demands MFA.
|
|
64
|
+
client = VhiClient(
|
|
65
|
+
username=username,
|
|
66
|
+
password=password,
|
|
67
|
+
mfa_callback=my_cli_mfa_callback
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# 2. Login
|
|
72
|
+
print("Logging in...")
|
|
73
|
+
client.login()
|
|
74
|
+
print("Login successful!")
|
|
75
|
+
|
|
76
|
+
# 3. Retrieve Claims
|
|
77
|
+
print("\nRetrieving claims...")
|
|
78
|
+
statements = client.get_claims()
|
|
79
|
+
|
|
80
|
+
if not statements:
|
|
81
|
+
print("No claim statements found.")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
for claim in statements:
|
|
85
|
+
print(f"- Claim {claim.claim_id} | Date: {claim.date_of_service} | Status: {claim.status}")
|
|
86
|
+
|
|
87
|
+
# 4. Download a Document
|
|
88
|
+
# We will download the first available document as an example
|
|
89
|
+
first_claim = statements[0]
|
|
90
|
+
if first_claim.document_id:
|
|
91
|
+
download_path = f"{first_claim.claim_id}.pdf"
|
|
92
|
+
print(f"\nDownloading document {first_claim.document_id} to {download_path}...")
|
|
93
|
+
client.download_document(first_claim.document_id, download_path)
|
|
94
|
+
print("Download complete!")
|
|
95
|
+
|
|
96
|
+
except VhiAuthenticationError as e:
|
|
97
|
+
print(f"Authentication failed: {e}")
|
|
98
|
+
except VhiApiError as e:
|
|
99
|
+
print(f"API Error occurred: {e}")
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Error Handling
|
|
106
|
+
|
|
107
|
+
The library provides structural custom exceptions:
|
|
108
|
+
- `VhiAuthenticationError`: Raised when login fails due to invalid credentials.
|
|
109
|
+
- `VhiMfaRequiredError`: Raised when the server requests MFA but you didn't provide a callback, or when the callback returns an invalid OTP.
|
|
110
|
+
- `VhiApiError`: Raised for any other 4xx or 5xx API errors. Contains standard HTTP status codes for debugging.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
vhi/__init__.py,sha256=ytq-BfovdJwn0P833AR1Zmli8Equ5fUU1fQ33fRdMUg,275
|
|
2
|
+
vhi/client.py,sha256=24NgKrzhQC0VkbSZstdkx2VW-IMHXEIlwPOa7Hq0uCY,8131
|
|
3
|
+
vhi/exceptions.py,sha256=5IcdRp-QoEUllPywhWLug6sHp1RTUzUETm-TGSenPSg,591
|
|
4
|
+
vhi/models.py,sha256=8QUa08OOtKFiraPFYpyzwQApH6mqwum2ssVp9pYlG1E,560
|
|
5
|
+
vhi_python-0.1.0.dist-info/METADATA,sha256=cbdpGDkZUWJXqYhAyKyNC9SBhUtOxoqSyQCpplWERLU,3807
|
|
6
|
+
vhi_python-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
vhi_python-0.1.0.dist-info/top_level.txt,sha256=IS0GUlyapVqQTomxbPnevKR6YgXGoQsgdAQ8RYDQki0,4
|
|
8
|
+
vhi_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vhi
|