ipulse-shared-core-ftredge 10.1.1__tar.gz → 11.1.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-10.1.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-11.1.1}/PKG-INFO +1 -1
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/setup.py +1 -1
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +56 -34
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +8 -5
- ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge/utils/__init__.py +1 -0
- ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge/utils/json_encoder.py +62 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
- ipulse_shared_core_ftredge-10.1.1/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -1
- ipulse_shared_core_ftredge-10.1.1/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -13
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_status.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
- {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 11.1.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
|
+
version='11.1.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=[
|
|
@@ -9,4 +9,4 @@ from .models import ( UserAuth, UserProfile,Subscription,
|
|
|
9
9
|
from .services import (BaseFirestoreService,BaseServiceException, ResourceNotFoundError, AuthorizationError,
|
|
10
10
|
ValidationError)
|
|
11
11
|
|
|
12
|
-
from .utils import (
|
|
12
|
+
from .utils import (EnsureJSONEncoderCompatibility)
|
|
@@ -2,16 +2,19 @@ import os
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Optional, Iterable, Dict, Any, List
|
|
4
4
|
from datetime import datetime, timedelta, timezone
|
|
5
|
+
import json
|
|
5
6
|
import httpx
|
|
6
7
|
from fastapi import HTTPException, Request
|
|
7
8
|
from google.cloud import firestore
|
|
8
9
|
from ipulse_shared_core_ftredge.services import ServiceError, AuthorizationError, ResourceNotFoundError
|
|
9
10
|
from ipulse_shared_core_ftredge.models import UserStatus
|
|
11
|
+
from ipulse_shared_core_ftredge.utils.json_encoder import convert_to_json_serializable
|
|
10
12
|
|
|
11
13
|
# Constants
|
|
12
14
|
USERS_STATUS_COLLECTION_NAME = UserStatus.get_collection_name()
|
|
13
15
|
USERS_STATUS_DOC_REF = "userstatus_"
|
|
14
16
|
CACHE_TTL = 60 # 60 seconds
|
|
17
|
+
|
|
15
18
|
class UserStatusCache:
|
|
16
19
|
"""Manages user status caching with dynamic invalidation"""
|
|
17
20
|
def __init__(self):
|
|
@@ -175,41 +178,78 @@ async def authorizeAPIRequest(
|
|
|
175
178
|
additional_info={"path": str(request.url)}
|
|
176
179
|
)
|
|
177
180
|
|
|
178
|
-
|
|
179
181
|
# Determine if we need fresh status
|
|
180
182
|
force_fresh = _should_force_fresh_status(request)
|
|
181
183
|
userstatus, cache_used = await get_userstatus(user_uid, db, force_fresh=force_fresh)
|
|
182
184
|
|
|
183
|
-
# Prepare authorization input
|
|
184
|
-
|
|
185
|
+
# Prepare authorization input that matches OPA expectations
|
|
186
|
+
# Extract required values from user status
|
|
187
|
+
primary_usertype = userstatus.get("primary_usertype")
|
|
188
|
+
secondary_usertypes = userstatus.get("secondary_usertypes", [])
|
|
189
|
+
|
|
190
|
+
# Extract IAM domain permissions
|
|
191
|
+
iam_domain_permissions = userstatus.get("iam_domain_permissions", {})
|
|
192
|
+
|
|
193
|
+
# Format the authz_input to match what the OPA policies expect
|
|
194
|
+
authz_input = {
|
|
185
195
|
"api_url": request.url.path,
|
|
186
196
|
"requestor": {
|
|
187
197
|
"uid": user_uid,
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
198
|
+
"primary_usertype": primary_usertype,
|
|
199
|
+
"secondary_usertypes": secondary_usertypes,
|
|
200
|
+
"usertypes": [primary_usertype] + secondary_usertypes if primary_usertype else secondary_usertypes,
|
|
201
|
+
"email_verified": request.state.user.get("email_verified", False),
|
|
202
|
+
"iam_domain_permissions": iam_domain_permissions,
|
|
203
|
+
"sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits", 0),
|
|
204
|
+
"extra_insight_credits": userstatus.get("extra_insight_credits", 0)
|
|
194
205
|
},
|
|
195
206
|
"method": request.method.lower(),
|
|
196
207
|
"request_resource_fields": request_resource_fields
|
|
197
208
|
}
|
|
198
209
|
|
|
199
|
-
|
|
210
|
+
# Convert any non-serializable objects to JSON serializable format
|
|
211
|
+
# Using the unified utility from utils
|
|
212
|
+
json_safe_authz_input = convert_to_json_serializable(authz_input)
|
|
213
|
+
|
|
200
214
|
# Query OPA
|
|
201
215
|
opa_url = f"{os.getenv('OPA_SERVER_URL', 'http://localhost:8181')}{os.getenv('OPA_DECISION_PATH', '/v1/data/http/authz/ingress/decision')}"
|
|
202
216
|
logger.debug(f"Attempting to connect to OPA at: {opa_url}")
|
|
203
|
-
logger.debug(f"Authorization input: {
|
|
217
|
+
logger.debug(f"Authorization input: {authz_input}")
|
|
218
|
+
|
|
204
219
|
async with httpx.AsyncClient() as client:
|
|
205
220
|
try:
|
|
206
221
|
response = await client.post(
|
|
207
222
|
opa_url,
|
|
208
|
-
json={"input":
|
|
223
|
+
json={"input": json_safe_authz_input},
|
|
209
224
|
timeout=5.0 # 5 seconds timeout
|
|
210
225
|
)
|
|
211
226
|
logger.debug(f"OPA Response Status: {response.status_code}")
|
|
212
227
|
logger.debug(f"OPA Response Body: {response.text}")
|
|
228
|
+
|
|
229
|
+
if response.status_code != 200:
|
|
230
|
+
logger.error(f"OPA authorization failed: {response.text}")
|
|
231
|
+
raise HTTPException(
|
|
232
|
+
status_code=500,
|
|
233
|
+
detail="Authorization service error"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
result = response.json()
|
|
237
|
+
logger.debug(f"Parsed OPA response: {result}")
|
|
238
|
+
|
|
239
|
+
if not result.get("result", {}).get("allow", False):
|
|
240
|
+
logger.error(f"Authorization denied: {result}")
|
|
241
|
+
raise AuthorizationError(
|
|
242
|
+
action=f"{request.method} {request.url.path}",
|
|
243
|
+
additional_info={
|
|
244
|
+
"user_uid": user_uid,
|
|
245
|
+
"resource_fields": request_resource_fields,
|
|
246
|
+
"opa_decision": result.get("result", {})
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Extract credit check information from the OPA response
|
|
251
|
+
credit_check = result.get("result", {}).get("credit_check", {})
|
|
252
|
+
|
|
213
253
|
except httpx.RequestError as e:
|
|
214
254
|
logger.error(f"Failed to connect to OPA: {str(e)}")
|
|
215
255
|
raise ServiceError(
|
|
@@ -221,39 +261,21 @@ async def authorizeAPIRequest(
|
|
|
221
261
|
"connection_error": str(e)
|
|
222
262
|
}
|
|
223
263
|
) from e
|
|
224
|
-
if response.status_code != 200:
|
|
225
|
-
logger.error(f"OPA authorization failed: {response.text}")
|
|
226
|
-
raise HTTPException(
|
|
227
|
-
status_code=500,
|
|
228
|
-
detail="Authorization service error"
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
result = response.json()
|
|
232
|
-
if not result.get("result", {}).get("allow", False):
|
|
233
|
-
raise AuthorizationError(
|
|
234
|
-
action=f"{request.method} {request.url.path}",
|
|
235
|
-
additional_info={
|
|
236
|
-
"user_uid": user_uid,
|
|
237
|
-
"resource_fields": request_resource_fields
|
|
238
|
-
}
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
# Extract credit check information from the OPA response
|
|
242
|
-
credit_check = {}
|
|
243
|
-
if "credit_check" in result.get("result", {}):
|
|
244
|
-
credit_check = result["result"]["credit_check"]
|
|
245
264
|
|
|
246
265
|
# More descriptive metadata about the data freshness
|
|
247
266
|
return {
|
|
248
267
|
"used_cached_status": cache_used,
|
|
249
268
|
"required_fresh_status": force_fresh,
|
|
250
269
|
"status_retrieved_at": datetime.now(timezone.utc).isoformat(),
|
|
251
|
-
"credit_check": credit_check
|
|
270
|
+
"credit_check": credit_check,
|
|
271
|
+
"allow_all_fields": result.get("result", {}).get("allow_all_fields", False),
|
|
272
|
+
"allowed_fields": result.get("result", {}).get("allowed_fields", [])
|
|
252
273
|
}
|
|
253
274
|
|
|
254
275
|
except (AuthorizationError, ResourceNotFoundError):
|
|
255
276
|
raise
|
|
256
277
|
except Exception as e:
|
|
278
|
+
logger.exception(f"Exception in authorizeAPIRequest: {e}")
|
|
257
279
|
raise ServiceError(
|
|
258
280
|
operation="API authorization",
|
|
259
281
|
error=e,
|
|
@@ -3,7 +3,7 @@ import datetime as dt
|
|
|
3
3
|
import json
|
|
4
4
|
from pydantic import BaseModel, ConfigDict
|
|
5
5
|
from fastapi.responses import JSONResponse
|
|
6
|
-
from ipulse_shared_core_ftredge.utils import
|
|
6
|
+
from ipulse_shared_core_ftredge.utils.json_encoder import EnsureJSONEncoderCompatibility, convert_to_json_serializable
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
T = TypeVar('T')
|
|
@@ -27,7 +27,7 @@ class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
|
|
|
27
27
|
|
|
28
28
|
class CustomJSONResponse(JSONResponse):
|
|
29
29
|
def render(self, content) -> bytes:
|
|
30
|
-
#
|
|
30
|
+
# First preprocess content with our utility function
|
|
31
31
|
if isinstance(content, dict) and "data" in content and hasattr(content["data"], "model_dump"):
|
|
32
32
|
# If content["data"] is a Pydantic model, use model_dump with exclude_unset=True
|
|
33
33
|
# and exclude_computed=True to prevent serialization of computed fields
|
|
@@ -37,12 +37,15 @@ class CustomJSONResponse(JSONResponse):
|
|
|
37
37
|
exclude_computed=True
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
#
|
|
40
|
+
# Now convert all problematic types to JSON serializable values
|
|
41
|
+
json_safe_content = convert_to_json_serializable(content)
|
|
42
|
+
|
|
43
|
+
# Use the CustomJSONEncoder for additional safety
|
|
41
44
|
return json.dumps(
|
|
42
|
-
|
|
45
|
+
json_safe_content,
|
|
43
46
|
ensure_ascii=False,
|
|
44
47
|
allow_nan=False,
|
|
45
48
|
indent=None,
|
|
46
49
|
separators=(",", ":"),
|
|
47
|
-
|
|
50
|
+
cls=EnsureJSONEncoderCompatibility
|
|
48
51
|
).encode("utf-8")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .json_encoder import EnsureJSONEncoderCompatibility
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from google.cloud.firestore_v1._helpers import DatetimeWithNanoseconds
|
|
5
|
+
from google.api_core import datetime_helpers
|
|
6
|
+
|
|
7
|
+
class EnsureJSONEncoderCompatibility(json.JSONEncoder):
|
|
8
|
+
"""Custom JSON encoder that handles Firestore datetime types and other non-serializable objects."""
|
|
9
|
+
def default(self, obj):
|
|
10
|
+
# Handle datetime types
|
|
11
|
+
if isinstance(obj, (datetime, DatetimeWithNanoseconds, datetime_helpers.DatetimeWithNanoseconds)):
|
|
12
|
+
return obj.isoformat()
|
|
13
|
+
# Handle enum types
|
|
14
|
+
elif isinstance(obj, Enum):
|
|
15
|
+
return obj.value
|
|
16
|
+
# Handle pydantic models
|
|
17
|
+
elif hasattr(obj, 'model_dump'):
|
|
18
|
+
return obj.model_dump()
|
|
19
|
+
# Default behavior for other types
|
|
20
|
+
return super().default(obj)
|
|
21
|
+
|
|
22
|
+
def convert_to_json_serializable(obj):
|
|
23
|
+
"""
|
|
24
|
+
Recursively convert objects to JSON serializable format.
|
|
25
|
+
Handles datetime objects, Enums, and nested structures.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
obj: Any Python object that might contain non-serializable types
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The object with all non-serializable types converted to serializable ones
|
|
32
|
+
"""
|
|
33
|
+
# Handle None
|
|
34
|
+
if obj is None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
# Handle datetime objects (including Firestore's DatetimeWithNanoseconds)
|
|
38
|
+
if hasattr(obj, 'isoformat'):
|
|
39
|
+
return obj.isoformat()
|
|
40
|
+
|
|
41
|
+
# Handle Enum values
|
|
42
|
+
elif isinstance(obj, Enum):
|
|
43
|
+
return obj.value
|
|
44
|
+
|
|
45
|
+
# Handle dictionaries
|
|
46
|
+
elif isinstance(obj, dict):
|
|
47
|
+
return {key: convert_to_json_serializable(value) for key, value in obj.items()}
|
|
48
|
+
|
|
49
|
+
# Handle lists and tuples
|
|
50
|
+
elif isinstance(obj, (list, tuple)):
|
|
51
|
+
return [convert_to_json_serializable(item) for item in obj]
|
|
52
|
+
|
|
53
|
+
# Handle sets
|
|
54
|
+
elif isinstance(obj, set):
|
|
55
|
+
return [convert_to_json_serializable(item) for item in obj]
|
|
56
|
+
|
|
57
|
+
# Handle Pydantic models and other objects with model_dump method
|
|
58
|
+
elif hasattr(obj, 'model_dump'):
|
|
59
|
+
return convert_to_json_serializable(obj.model_dump())
|
|
60
|
+
|
|
61
|
+
# Return primitive types as-is
|
|
62
|
+
return obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 11.1.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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .json_encoder import CustomJSONEncoder
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from google.cloud.firestore_v1._helpers import DatetimeWithNanoseconds
|
|
4
|
-
from google.api_core import datetime_helpers
|
|
5
|
-
|
|
6
|
-
class CustomJSONEncoder(json.JSONEncoder):
|
|
7
|
-
"""Custom JSON encoder that handles Firestore datetime types."""
|
|
8
|
-
def default(self, obj):
|
|
9
|
-
if isinstance(obj, (datetime, DatetimeWithNanoseconds)):
|
|
10
|
-
return obj.isoformat()
|
|
11
|
-
if isinstance(obj, datetime_helpers.DatetimeWithNanoseconds):
|
|
12
|
-
return obj.isoformat()
|
|
13
|
-
return super().default(obj)
|
|
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
|