ipulse-shared-core-ftredge 6.7.1__tar.gz → 6.9.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.9.1}/PKG-INFO +2 -2
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/setup.py +2 -2
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/__init__.py +4 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/dependencies/authorization_api.py +55 -39
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +5 -2
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/services/__init__.py +1 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +16 -16
- ipulse_shared_core_ftredge-6.7.1/src/ipulse_shared_core_ftredge/services/base_exceptions.py → ipulse_shared_core_ftredge-6.9.1/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py +5 -5
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +2 -2
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +1 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -1
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/dependencies/auth_router.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/dependencies/database.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/dependencies/token_validation.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.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.9.1}/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.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.9.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.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.9.1}/src/ipulse_shared_core_ftredge/models/user_status.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-6.7.1 → ipulse_shared_core_ftredge-6.9.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.9.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.9.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
|
|
@@ -9,7 +9,7 @@ Requires-Dist: pydantic[email]~=2.5
|
|
|
9
9
|
Requires-Dist: python-dateutil~=2.8
|
|
10
10
|
Requires-Dist: fastapi~=0.115.8
|
|
11
11
|
Requires-Dist: pytest~=7.1
|
|
12
|
-
Requires-Dist: ipulse_shared_base_ftredge>=5.
|
|
12
|
+
Requires-Dist: ipulse_shared_base_ftredge>=5.7.1
|
|
13
13
|
Dynamic: author
|
|
14
14
|
Dynamic: home-page
|
|
15
15
|
Dynamic: requires-dist
|
|
@@ -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.9.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=[
|
|
@@ -12,7 +12,7 @@ setup(
|
|
|
12
12
|
'python-dateutil~=2.8',
|
|
13
13
|
'fastapi~=0.115.8',
|
|
14
14
|
'pytest~=7.1',
|
|
15
|
-
'ipulse_shared_base_ftredge>=5.
|
|
15
|
+
'ipulse_shared_base_ftredge>=5.7.1',
|
|
16
16
|
],
|
|
17
17
|
author='Russlan Ramdowar',
|
|
18
18
|
description='Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.',
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# pylint: disable=missing-module-docstring
|
|
2
2
|
from .models import ( UserAuth, UserProfile,Subscription,
|
|
3
3
|
UserStatus, UserProfileUpdate,
|
|
4
|
-
OrganizationProfile, BaseAPIResponse,
|
|
4
|
+
OrganizationProfile, BaseAPIResponse,
|
|
5
|
+
CustomJSONResponse )
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
from .services import (BaseFirestoreService,BaseServiceException, ResourceNotFoundError, AuthorizationError,
|
|
9
10
|
ValidationError)
|
|
11
|
+
|
|
12
|
+
from .utils import (CustomJSONEncoder)
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ from .user_status import UserStatus
|
|
|
4
4
|
from .user_profile_update import UserProfileUpdate
|
|
5
5
|
from .user_auth import UserAuth
|
|
6
6
|
from .organization_profile import OrganizationProfile
|
|
7
|
-
from .base_api_response import BaseAPIResponse, CustomJSONResponse
|
|
7
|
+
from .base_api_response import BaseAPIResponse , CustomJSONResponse
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
@@ -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
|
|
3
|
+
import json
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
5
|
from fastapi.responses import JSONResponse
|
|
5
6
|
from ipulse_shared_core_ftredge.utils import CustomJSONEncoder
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
T = TypeVar('T')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from .base_firestore_service import BaseFirestoreService
|
|
2
2
|
|
|
3
|
-
from .
|
|
3
|
+
from .base_service_exceptions import (BaseServiceException, ResourceNotFoundError, AuthorizationError,
|
|
4
4
|
ValidationError ,ServiceError)
|
|
5
5
|
from .servicemon import Servicemon
|
|
6
6
|
from .fastapiservicemon import FastAPIServiceMon
|
|
@@ -4,13 +4,13 @@ import logging
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from google.cloud import firestore
|
|
7
|
-
from .
|
|
7
|
+
from .base_service_exceptions import ResourceNotFoundError, ValidationError, ServiceError
|
|
8
8
|
|
|
9
9
|
T = TypeVar('T', bound=BaseModel)
|
|
10
10
|
|
|
11
11
|
class BaseFirestoreService(Generic[T]):
|
|
12
12
|
"""Base class for Firestore services with common CRUD operations"""
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
def __init__(self, db: firestore.Client, collection_name: str, resource_type: str, logger: logging.Logger):
|
|
15
15
|
self.db = db
|
|
16
16
|
self.collection_name = collection_name
|
|
@@ -22,7 +22,7 @@ class BaseFirestoreService(Generic[T]):
|
|
|
22
22
|
try:
|
|
23
23
|
current_time = datetime.now(timezone.utc)
|
|
24
24
|
doc_data = data.model_dump(mode='json')
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
# Add audit fields
|
|
27
27
|
doc_data.update({
|
|
28
28
|
'creat_date': current_time.isoformat(),
|
|
@@ -33,7 +33,7 @@ class BaseFirestoreService(Generic[T]):
|
|
|
33
33
|
|
|
34
34
|
doc_ref = self.db.collection(self.collection_name).document(doc_id)
|
|
35
35
|
doc_ref.set(doc_data)
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
self.logger.info(f"Created {self.resource_type}: {doc_id}")
|
|
38
38
|
return doc_data
|
|
39
39
|
|
|
@@ -61,7 +61,7 @@ class BaseFirestoreService(Generic[T]):
|
|
|
61
61
|
'updt_date': current_time.isoformat(),
|
|
62
62
|
'updt_by_user': creator_uid
|
|
63
63
|
})
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
doc_ref = self.db.collection(self.collection_name).document(doc_data.get('id'))
|
|
66
66
|
batch.set(doc_ref, doc_data)
|
|
67
67
|
created_docs.append(doc_data)
|
|
@@ -83,39 +83,39 @@ class BaseFirestoreService(Generic[T]):
|
|
|
83
83
|
"""Get a document by ID with standardized error handling"""
|
|
84
84
|
doc_ref = self.db.collection(self.collection_name).document(doc_id)
|
|
85
85
|
doc = doc_ref.get()
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
if not doc.exists:
|
|
88
88
|
raise ResourceNotFoundError(
|
|
89
89
|
resource_type=self.resource_type,
|
|
90
90
|
resource_id=doc_id,
|
|
91
91
|
additional_info={"collection": self.collection_name}
|
|
92
92
|
)
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
return doc.to_dict()
|
|
95
95
|
|
|
96
96
|
async def update_document(self, doc_id: str, update_data: Dict[str, Any], updater_uid: str) -> Dict[str, Any]:
|
|
97
97
|
"""Standard update method with validation and audit fields"""
|
|
98
98
|
try:
|
|
99
99
|
doc_ref = self.db.collection(self.collection_name).document(doc_id)
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
if not doc_ref.get().exists:
|
|
102
102
|
raise ResourceNotFoundError(
|
|
103
103
|
resource_type=self.resource_type,
|
|
104
104
|
resource_id=doc_id,
|
|
105
105
|
additional_info={"collection": self.collection_name}
|
|
106
106
|
)
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
valid_fields = self._validate_update_fields(update_data)
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
# Add audit fields
|
|
111
111
|
valid_fields.update({
|
|
112
112
|
'updt_date': datetime.now(timezone.utc).isoformat(),
|
|
113
113
|
'updt_by_user': updater_uid
|
|
114
114
|
})
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
doc_ref.update(valid_fields)
|
|
117
117
|
return doc_ref.get().to_dict()
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
except (ResourceNotFoundError, ValidationError):
|
|
120
120
|
raise
|
|
121
121
|
except Exception as e:
|
|
@@ -136,7 +136,7 @@ class BaseFirestoreService(Generic[T]):
|
|
|
136
136
|
resource_type=self.resource_type,
|
|
137
137
|
resource_id=doc_id
|
|
138
138
|
)
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
doc_ref.delete()
|
|
141
141
|
self.logger.info(f"Deleted {self.resource_type}: {doc_id}")
|
|
142
142
|
|
|
@@ -155,17 +155,17 @@ class BaseFirestoreService(Generic[T]):
|
|
|
155
155
|
"""Centralized update fields validation"""
|
|
156
156
|
if not isinstance(update_data, dict):
|
|
157
157
|
update_data = update_data.model_dump(exclude_unset=True)
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
valid_fields = {
|
|
160
160
|
k: v for k, v in update_data.items()
|
|
161
161
|
if v is not None and not (isinstance(v, (list, dict, set)) and len(v) == 0)
|
|
162
162
|
}
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
if not valid_fields:
|
|
165
165
|
raise ValidationError(
|
|
166
166
|
resource_type=self.resource_type,
|
|
167
167
|
detail="No valid fields to update",
|
|
168
168
|
resource_id=None
|
|
169
169
|
)
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
return valid_fields
|
|
@@ -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.9.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
|
|
@@ -9,7 +9,7 @@ Requires-Dist: pydantic[email]~=2.5
|
|
|
9
9
|
Requires-Dist: python-dateutil~=2.8
|
|
10
10
|
Requires-Dist: fastapi~=0.115.8
|
|
11
11
|
Requires-Dist: pytest~=7.1
|
|
12
|
-
Requires-Dist: ipulse_shared_base_ftredge>=5.
|
|
12
|
+
Requires-Dist: ipulse_shared_base_ftredge>=5.7.1
|
|
13
13
|
Dynamic: author
|
|
14
14
|
Dynamic: home-page
|
|
15
15
|
Dynamic: requires-dist
|
|
@@ -24,8 +24,8 @@ src/ipulse_shared_core_ftredge/models/user_profile.py
|
|
|
24
24
|
src/ipulse_shared_core_ftredge/models/user_profile_update.py
|
|
25
25
|
src/ipulse_shared_core_ftredge/models/user_status.py
|
|
26
26
|
src/ipulse_shared_core_ftredge/services/__init__.py
|
|
27
|
-
src/ipulse_shared_core_ftredge/services/base_exceptions.py
|
|
28
27
|
src/ipulse_shared_core_ftredge/services/base_firestore_service.py
|
|
28
|
+
src/ipulse_shared_core_ftredge/services/base_service_exceptions.py
|
|
29
29
|
src/ipulse_shared_core_ftredge/services/fastapiservicemon.py
|
|
30
30
|
src/ipulse_shared_core_ftredge/services/servicemon.py
|
|
31
31
|
src/ipulse_shared_core_ftredge/utils/__init__.py
|
|
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
|