vhi-python 0.1.2__tar.gz → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vhi-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A Python client library for the Vhi API, handling authentication, MFA callbacks, claims retrieval, and document downloads.
5
5
  Author-email: Agent <agent@example.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vhi-python"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "A Python client library for the Vhi API, handling authentication, MFA callbacks, claims retrieval, and document downloads."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,5 +1,10 @@
1
1
  import requests
2
2
  import os
3
+ import uuid
4
+ import json
5
+ import pickle
6
+ import hashlib
7
+ from pathlib import Path
3
8
  from typing import Callable, Optional, List
4
9
  from .exceptions import VhiAuthenticationError, VhiMfaRequiredError, VhiApiError
5
10
  from .models import ClaimStatement
@@ -11,7 +16,7 @@ class VhiClient:
11
16
  """
12
17
  DEFAULT_CONFIG_URL = "https://www.vhi.ie/myvhi/myclaimstatements" # Usually embedded here
13
18
 
14
- def __init__(self, username: str, password: str, mfa_callback: Optional[Callable[[], str]] = None):
19
+ def __init__(self, username: str, password: str, mfa_callback: Optional[Callable[[], str]] = None, cache_session: bool = True):
15
20
  """
16
21
  Initialize the VhiClient.
17
22
 
@@ -19,23 +24,68 @@ class VhiClient:
19
24
  username: User's email address.
20
25
  password: User's password.
21
26
  mfa_callback: A callable that returns the 2FA OTP string when invoked.
27
+ cache_session: Whether to cache session cookies locally to prevent re-authentication.
22
28
  """
23
29
  self.username = username
24
30
  self.password = password
25
31
  self.mfa_callback = mfa_callback
32
+ self.cache_session = cache_session
26
33
 
27
34
  # State management
28
35
  self.session = requests.Session()
29
36
 
30
37
  # Default Browser Headers to mimic browser and avoid WAF
31
38
  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",
39
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
40
+ "sec-ch-ua": '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"',
41
+ "sec-ch-ua-mobile": "?0",
42
+ "sec-ch-ua-platform": '"Windows"',
34
43
  })
35
44
 
36
45
  self.is_authenticated = False
37
46
  self.apis_base_url = "https://apis.vhi.ie" # Will be updated during discovery if necessary
38
47
 
48
+ def _get_cache_path(self) -> Path:
49
+ user_hash = hashlib.md5(self.username.encode('utf-8')).hexdigest()
50
+ cache_dir = Path.home() / ".vhi"
51
+ cache_dir.mkdir(exist_ok=True)
52
+ return cache_dir / f"session_{user_hash}.pkl"
53
+
54
+ def _save_session(self):
55
+ if not self.cache_session:
56
+ return
57
+ with open(self._get_cache_path(), 'wb') as f:
58
+ pickle.dump(self.session.cookies, f)
59
+
60
+ def _load_session(self) -> bool:
61
+ if not self.cache_session:
62
+ return False
63
+ cache_path = self._get_cache_path()
64
+ if cache_path.exists():
65
+ try:
66
+ with open(cache_path, 'rb') as f:
67
+ self.session.cookies = pickle.load(f)
68
+ return True
69
+ except Exception:
70
+ pass
71
+ return False
72
+
73
+ def _is_session_valid(self) -> bool:
74
+ try:
75
+ url = f"{self.apis_base_url}/claims/v1/statements"
76
+ headers = {
77
+ "Accept": "application/json",
78
+ "Origin": "https://app.vhi.ie",
79
+ "Referer": "https://app.vhi.ie/",
80
+ "Sec-Fetch-Mode": "cors",
81
+ "Sec-Fetch-Site": "same-site",
82
+ }
83
+ # Only test to see if 401 is returned
84
+ response = self.session.get(url, headers=headers, timeout=5)
85
+ return response.status_code == 200
86
+ except Exception:
87
+ return False
88
+
39
89
  def _fetch_environment_config(self):
40
90
  """
41
91
  Optional: Dynamically discovers the API base URL.
@@ -52,14 +102,28 @@ class VhiClient:
52
102
  """
53
103
  Performs the login flow. Handles the MFA challenge if required.
54
104
  """
55
- # Set proper fetch headers
105
+ if self._load_session():
106
+ if self._is_session_valid():
107
+ self.is_authenticated = True
108
+ return
109
+ else:
110
+ self.session.cookies.clear()
111
+
112
+ # Set proper fetch headers mimicking trace exactly
56
113
  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",
114
+ "accept": "application/json, text/plain, */*",
115
+ "accept-language": "en-IE,en-US;q=0.9,en;q=0.8,ja;q=0.7,en-GB;q=0.6,de;q=0.5",
116
+ "cache-control": "no-cache",
117
+ "content-type": "application/json",
118
+ "dnt": "1",
119
+ "origin": "https://app.vhi.ie",
120
+ "pragma": "no-cache",
121
+ "priority": "u=1, i",
122
+ "referer": "https://app.vhi.ie/",
123
+ "sec-fetch-dest": "empty",
124
+ "sec-fetch-mode": "cors",
125
+ "sec-fetch-site": "same-site",
126
+ "user-session-id": str(uuid.uuid4())
63
127
  }
64
128
 
65
129
  payload = {
@@ -67,10 +131,13 @@ class VhiClient:
67
131
  "usercred": self.password
68
132
  }
69
133
 
134
+ # Format strictly without spaces to bypass strict WAF parsers
135
+ raw_payload = json.dumps(payload, separators=(',', ':'))
136
+
70
137
  # 1. Primary Authentication
71
138
  login_url = f"{self.apis_base_url}/api/myvhilogin/login"
72
139
 
73
- response = self.session.post(login_url, json=payload, headers=headers)
140
+ response = self.session.post(login_url, data=raw_payload, headers=headers)
74
141
 
75
142
  if response.status_code == 200:
76
143
  try:
@@ -98,12 +165,13 @@ class VhiClient:
98
165
  self._handle_mfa_challenge(state_token, verify_url, headers)
99
166
  else:
100
167
  self.is_authenticated = True
168
+ self._save_session()
101
169
  return
102
170
 
103
171
  elif response.status_code == 401:
104
172
  raise VhiAuthenticationError("Invalid credentials provided.")
105
173
  else:
106
- raise VhiApiError(f"Login failed with status {response.status_code}", status_code=response.status_code, response=response)
174
+ raise VhiApiError(f"Login failed with status {response.status_code}: {response.text}", status_code=response.status_code, response=response)
107
175
 
108
176
  def _handle_mfa_challenge(self, state_token: str, verify_url: str, base_headers: dict):
109
177
  """
@@ -127,7 +195,7 @@ class VhiClient:
127
195
 
128
196
  if response.status_code == 200:
129
197
  self.is_authenticated = True
130
-
198
+ self._save_session()
131
199
  # The session cookie jar has now been populated with the authorized session
132
200
  # Or we might need to extract a Bearer token. This implementation relies
133
201
  # on cookies being correctly set via Set-Cookie headers by the API.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vhi-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A Python client library for the Vhi API, handling authentication, MFA callbacks, claims retrieval, and document downloads.
5
5
  Author-email: Agent <agent@example.com>
6
6
  License: MIT
@@ -7,7 +7,7 @@ from vhi.exceptions import VhiAuthenticationError, VhiMfaRequiredError, VhiApiEr
7
7
 
8
8
  @responses.activate
9
9
  def test_login_success_no_mfa():
10
- client = VhiClient("test@example.com", "password")
10
+ client = VhiClient("test@example.com", "password", cache_session=False)
11
11
 
12
12
  responses.add(
13
13
  responses.POST,
@@ -24,7 +24,7 @@ def test_login_requires_mfa_success():
24
24
  def mfa_callback():
25
25
  return "123456"
26
26
 
27
- client = VhiClient("test@example.com", "password", mfa_callback=mfa_callback)
27
+ client = VhiClient("test@example.com", "password", mfa_callback=mfa_callback, cache_session=False)
28
28
 
29
29
  # Mock primary login returning 200 with MFA_REQUIRED
30
30
  responses.add(
@@ -51,7 +51,7 @@ def test_login_requires_mfa_success():
51
51
 
52
52
  @responses.activate
53
53
  def test_login_invalid_credentials():
54
- client = VhiClient("test@example.com", "wrong")
54
+ client = VhiClient("test@example.com", "wrong", cache_session=False)
55
55
 
56
56
  responses.add(
57
57
  responses.POST,
@@ -64,7 +64,7 @@ def test_login_invalid_credentials():
64
64
 
65
65
  @responses.activate
66
66
  def test_get_claims_success():
67
- client = VhiClient("test@example.com", "password")
67
+ client = VhiClient("test@example.com", "password", cache_session=False)
68
68
  client.is_authenticated = True # Mock existing session
69
69
 
70
70
  responses.add(
@@ -91,7 +91,7 @@ def test_get_claims_success():
91
91
 
92
92
  @responses.activate
93
93
  def test_download_document_success():
94
- client = VhiClient("test@example.com", "password")
94
+ client = VhiClient("test@example.com", "password", cache_session=False)
95
95
  client.is_authenticated = True
96
96
 
97
97
  pdf_content = b"%PDF-1.4 mock pdf content"
File without changes
File without changes
File without changes