ipulse-shared-core-ftredge 6.7.1__tar.gz → 6.8.1__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.
Potentially problematic release.
This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.
- {ipulse_shared_core_ftredge-6.7.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-6.8.1}/PKG-INFO +1 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/setup.py +1 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/__init__.py +2 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/dependencies/authorization_api.py +55 -39
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +6 -3
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/services/base_exceptions.py +5 -5
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/dependencies/auth_router.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/dependencies/database.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/dependencies/token_validation.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/__init__.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/resource_catalog_item.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/models/user_status.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.8.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.8.1
|
|
4
4
|
Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
|
|
5
5
|
Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
|
|
6
6
|
Author: Russlan Ramdowar
|
|
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|
|
3
3
|
|
|
4
4
|
setup(
|
|
5
5
|
name='ipulse_shared_core_ftredge',
|
|
6
|
-
version='6.
|
|
6
|
+
version='6.8.1',
|
|
7
7
|
package_dir={'': 'src'}, # Specify the source directory
|
|
8
8
|
packages=find_packages(where='src'), # Look for packages in 'src'
|
|
9
9
|
install_requires=[
|
|
@@ -6,24 +6,25 @@ import httpx
|
|
|
6
6
|
from fastapi import HTTPException, Request
|
|
7
7
|
from google.cloud import firestore
|
|
8
8
|
from ipulse_shared_core_ftredge.services import ServiceError, AuthorizationError, ResourceNotFoundError
|
|
9
|
+
from ipulse_shared_core_ftredge.models import UserStatus
|
|
9
10
|
|
|
10
11
|
# Constants
|
|
11
|
-
USERS_STATUS_COLLECTION_NAME =
|
|
12
|
-
USERS_STATUS_DOC_REF = "
|
|
12
|
+
USERS_STATUS_COLLECTION_NAME = UserStatus.get_collection_name()
|
|
13
|
+
USERS_STATUS_DOC_REF = "userstatus_"
|
|
13
14
|
CACHE_TTL = 60 # 60 seconds
|
|
14
15
|
class UserStatusCache:
|
|
15
16
|
"""Manages user status caching with dynamic invalidation"""
|
|
16
17
|
def __init__(self):
|
|
17
18
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
18
19
|
self._timestamps: Dict[str, datetime] = {}
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
def get(self, user_uid: str) -> Optional[Dict[str, Any]]:
|
|
21
22
|
"""
|
|
22
23
|
Retrieves user status from cache if available and valid.
|
|
23
24
|
|
|
24
25
|
Args:
|
|
25
26
|
user_uid (str): The user ID.
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
"""
|
|
28
29
|
if user_uid in self._cache:
|
|
29
30
|
status_data = self._cache[user_uid]
|
|
@@ -33,7 +34,7 @@ class UserStatusCache:
|
|
|
33
34
|
return status_data
|
|
34
35
|
self.invalidate(user_uid)
|
|
35
36
|
return None
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
def set(self, user_uid: str, data: Dict[str, Any]) -> None:
|
|
38
39
|
"""
|
|
39
40
|
Sets user status data in the cache.
|
|
@@ -44,7 +45,7 @@ class UserStatusCache:
|
|
|
44
45
|
"""
|
|
45
46
|
self._cache[user_uid] = data
|
|
46
47
|
self._timestamps[user_uid] = datetime.now()
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
def invalidate(self, user_uid: str) -> None:
|
|
49
50
|
"""
|
|
50
51
|
Invalidates (removes) user status from the cache.
|
|
@@ -56,13 +57,13 @@ class UserStatusCache:
|
|
|
56
57
|
self._timestamps.pop(user_uid, None)
|
|
57
58
|
|
|
58
59
|
# Global cache instance
|
|
59
|
-
|
|
60
|
+
userstatus_cache = UserStatusCache()
|
|
60
61
|
|
|
61
62
|
# Replace the logger dependency with a standard logger
|
|
62
63
|
logger = logging.getLogger(__name__)
|
|
63
64
|
|
|
64
|
-
async def
|
|
65
|
-
user_uid: str,
|
|
65
|
+
async def get_userstatus(
|
|
66
|
+
user_uid: str,
|
|
66
67
|
db: firestore.Client, # Note: This expects the actual client, not a Depends
|
|
67
68
|
force_fresh: bool = False
|
|
68
69
|
) -> tuple[Dict[str, Any], bool]:
|
|
@@ -71,39 +72,39 @@ async def get_user_status(
|
|
|
71
72
|
"""
|
|
72
73
|
cache_used = False
|
|
73
74
|
if not force_fresh:
|
|
74
|
-
cached_status =
|
|
75
|
+
cached_status = userstatus_cache.get(user_uid)
|
|
75
76
|
if cached_status:
|
|
76
77
|
cache_used = True
|
|
77
78
|
return cached_status, cache_used
|
|
78
79
|
|
|
79
80
|
try:
|
|
80
81
|
# Get reference to the document
|
|
81
|
-
|
|
82
|
-
user_ref = db.collection(USERS_STATUS_COLLECTION_NAME).document(
|
|
83
|
-
|
|
82
|
+
userstatus_id = USERS_STATUS_DOC_REF + user_uid
|
|
83
|
+
user_ref = db.collection(USERS_STATUS_COLLECTION_NAME).document(userstatus_id)
|
|
84
|
+
|
|
84
85
|
# Get the document
|
|
85
86
|
snapshot = user_ref.get()
|
|
86
87
|
if not snapshot.exists:
|
|
87
88
|
raise ResourceNotFoundError(
|
|
88
|
-
resource_type="
|
|
89
|
-
resource_id=
|
|
89
|
+
resource_type="authorization userstatus",
|
|
90
|
+
resource_id=userstatus_id,
|
|
90
91
|
additional_info={"user_uid": user_uid}
|
|
91
92
|
)
|
|
92
|
-
|
|
93
|
+
|
|
93
94
|
status_data = snapshot.to_dict()
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
# Only cache if not forced fresh
|
|
96
97
|
if not force_fresh:
|
|
97
|
-
|
|
98
|
+
userstatus_cache.set(user_uid, status_data)
|
|
98
99
|
return status_data, cache_used
|
|
99
|
-
|
|
100
|
+
|
|
100
101
|
except ResourceNotFoundError:
|
|
101
102
|
raise
|
|
102
103
|
except Exception as e:
|
|
103
104
|
raise ServiceError(
|
|
104
105
|
operation=f"fetching user status",
|
|
105
106
|
error=e,
|
|
106
|
-
resource_type="
|
|
107
|
+
resource_type="userstatus",
|
|
107
108
|
resource_id=user_uid,
|
|
108
109
|
additional_info={
|
|
109
110
|
"force_fresh": force_fresh,
|
|
@@ -131,18 +132,18 @@ async def extract_request_fields(request: Request) -> Optional[List[str]]:
|
|
|
131
132
|
if isinstance(body, dict):
|
|
132
133
|
if request.method == "PATCH":
|
|
133
134
|
return _validate_resource_fields(body)
|
|
134
|
-
|
|
135
|
+
if request.method == "POST":
|
|
135
136
|
# For POST, we want to include all fields being set
|
|
136
137
|
return list(body.keys())
|
|
137
138
|
elif hasattr(body, 'model_dump'):
|
|
138
139
|
data = body.model_dump(exclude_unset=True)
|
|
139
140
|
if request.method == "PATCH":
|
|
140
141
|
return _validate_resource_fields(data)
|
|
141
|
-
|
|
142
|
+
if request.method == "POST":
|
|
142
143
|
return list(data.keys())
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
return None
|
|
145
|
-
|
|
146
|
+
|
|
146
147
|
except Exception as e:
|
|
147
148
|
logger.error(f"Error extracting fields from request body: {str(e)}")
|
|
148
149
|
return None
|
|
@@ -172,8 +173,8 @@ async def authorizeAPIRequest(
|
|
|
172
173
|
|
|
173
174
|
# Determine if we need fresh status
|
|
174
175
|
force_fresh = _should_force_fresh_status(request)
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
userstatus, cache_used = await get_userstatus(user_uid, db, force_fresh=force_fresh)
|
|
177
|
+
|
|
177
178
|
# Prepare authorization input
|
|
178
179
|
auth_input = {
|
|
179
180
|
"api_url": request.url.path,
|
|
@@ -181,10 +182,10 @@ async def authorizeAPIRequest(
|
|
|
181
182
|
"uid": user_uid,
|
|
182
183
|
"usertypes": request.state.user.get("usertypes"),
|
|
183
184
|
"email_verified": request.state.user.get("email_verified"),
|
|
184
|
-
"iam_groups":
|
|
185
|
-
"subscriptions":
|
|
186
|
-
"sbscrptn_based_insight_credits":
|
|
187
|
-
"extra_insight_credits":
|
|
185
|
+
"iam_groups": userstatus.get("iam_groups"),
|
|
186
|
+
"subscriptions": userstatus.get("subscriptions"),
|
|
187
|
+
"sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits"),
|
|
188
|
+
"extra_insight_credits": userstatus.get("extra_insight_credits")
|
|
188
189
|
},
|
|
189
190
|
"method": request.method.lower(),
|
|
190
191
|
"request_resource_fields": request_resource_fields
|
|
@@ -192,13 +193,28 @@ async def authorizeAPIRequest(
|
|
|
192
193
|
|
|
193
194
|
# Query OPA
|
|
194
195
|
opa_url = f"{os.getenv('OPA_SERVER_URL', 'http://localhost:8181')}{os.getenv('OPA_DECISION_PATH', '/v1/data/http/authz/ingress/decision')}"
|
|
196
|
+
logger.debug(f"Attempting to connect to OPA at: {opa_url}")
|
|
197
|
+
logger.debug(f"Authorization input: {auth_input}")
|
|
195
198
|
async with httpx.AsyncClient() as client:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
try:
|
|
200
|
+
response = await client.post(
|
|
201
|
+
opa_url,
|
|
202
|
+
json={"input": auth_input},
|
|
203
|
+
timeout=5.0 # 5 seconds timeout
|
|
204
|
+
)
|
|
205
|
+
logger.debug(f"OPA Response Status: {response.status_code}")
|
|
206
|
+
logger.debug(f"OPA Response Body: {response.text}")
|
|
207
|
+
except httpx.RequestError as e:
|
|
208
|
+
logger.error(f"Failed to connect to OPA: {str(e)}")
|
|
209
|
+
raise ServiceError(
|
|
210
|
+
operation="API authorization",
|
|
211
|
+
error=e,
|
|
212
|
+
resource_type="authorization",
|
|
213
|
+
additional_info={
|
|
214
|
+
"opa_url": opa_url,
|
|
215
|
+
"connection_error": str(e)
|
|
216
|
+
}
|
|
217
|
+
) from e
|
|
202
218
|
if response.status_code != 200:
|
|
203
219
|
logger.error(f"OPA authorization failed: {response.text}")
|
|
204
220
|
raise HTTPException(
|
|
@@ -236,7 +252,7 @@ async def authorizeAPIRequest(
|
|
|
236
252
|
"user_uid": request.state.user.get('uid'),
|
|
237
253
|
"resource_fields": request_resource_fields
|
|
238
254
|
}
|
|
239
|
-
)
|
|
255
|
+
) from e
|
|
240
256
|
|
|
241
257
|
def _should_force_fresh_status(request: Request) -> bool:
|
|
242
258
|
"""
|
|
@@ -250,10 +266,10 @@ def _should_force_fresh_status(request: Request) -> bool:
|
|
|
250
266
|
]
|
|
251
267
|
# Methods that require fresh status
|
|
252
268
|
sensitive_methods = {'post', 'patch', 'put', 'delete'}
|
|
253
|
-
|
|
269
|
+
|
|
254
270
|
path = request.url.path.lower()
|
|
255
271
|
method = request.method.lower()
|
|
256
|
-
|
|
272
|
+
|
|
257
273
|
return (
|
|
258
274
|
any(pattern in path for pattern in credit_sensitive_patterns) or
|
|
259
275
|
method in sensitive_methods
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from typing import Generic, TypeVar, Optional, Any, Dict, List
|
|
2
|
-
from pydantic import BaseModel, ConfigDict
|
|
3
2
|
import datetime as dt
|
|
4
|
-
from fastapi.responses import JSONResponse
|
|
5
|
-
from ipulse_shared_core_ftredge.utils import CustomJSONEncoder
|
|
6
3
|
import json
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
from ipulse_shared_core_ftredge import CustomJSONEncoder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
T = TypeVar('T')
|
|
@@ -19,7 +19,7 @@ class BaseServiceException(HTTPException):
|
|
|
19
19
|
self.resource_id = resource_id
|
|
20
20
|
self.additional_info = additional_info or {}
|
|
21
21
|
self.original_error = original_error
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
# Get full traceback if there's an original error
|
|
24
24
|
if original_error:
|
|
25
25
|
self.traceback = ''.join(traceback.format_exception(
|
|
@@ -29,14 +29,14 @@ class BaseServiceException(HTTPException):
|
|
|
29
29
|
))
|
|
30
30
|
else:
|
|
31
31
|
self.traceback = ''.join(traceback.format_stack())
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
# Build detailed message
|
|
34
34
|
detail_msg = f"{detail}"
|
|
35
35
|
if resource_type:
|
|
36
36
|
detail_msg += f" [Resource Type: {resource_type}]"
|
|
37
37
|
if resource_id:
|
|
38
38
|
detail_msg += f" [ID: {resource_id}]"
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
super().__init__(status_code=status_code, detail=detail_msg)
|
|
41
41
|
|
|
42
42
|
def log_error(self, logger: logging.Logger):
|
|
@@ -48,7 +48,7 @@ class BaseServiceException(HTTPException):
|
|
|
48
48
|
"detail": self.detail,
|
|
49
49
|
**self.additional_info
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
log_message = f"""
|
|
53
53
|
Service Error Occurred:
|
|
54
54
|
Status Code: {self.status_code}
|
|
@@ -60,7 +60,7 @@ class BaseServiceException(HTTPException):
|
|
|
60
60
|
Traceback:
|
|
61
61
|
{self.traceback}
|
|
62
62
|
"""
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
logger.error(log_message, extra=error_context)
|
|
65
65
|
|
|
66
66
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.8.1
|
|
4
4
|
Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
|
|
5
5
|
Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
|
|
6
6
|
Author: Russlan Ramdowar
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|