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.

Files changed (36) hide show
  1. {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
  2. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/setup.py +1 -1
  3. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
  4. {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
  5. {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
  6. ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge/utils/__init__.py +1 -0
  7. ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge/utils/json_encoder.py +62 -0
  8. {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
  9. ipulse_shared_core_ftredge-10.1.1/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -1
  10. ipulse_shared_core_ftredge-10.1.1/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -13
  11. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/LICENCE +0 -0
  12. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/README.md +0 -0
  13. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/pyproject.toml +0 -0
  14. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/setup.cfg +0 -0
  15. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  16. {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
  17. {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
  18. {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
  19. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +0 -0
  20. {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
  21. {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
  22. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
  23. {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
  24. {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
  25. {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
  26. {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
  27. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
  28. {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
  29. {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
  30. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
  31. {ipulse_shared_core_ftredge-10.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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: 10.1.1
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='10.1.1',
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 (CustomJSONEncoder)
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
- auth_input = {
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
- "usertypes": request.state.user.get("usertypes"),
189
- "email_verified": request.state.user.get("email_verified"),
190
- "iam_groups": userstatus.get("iam_groups"),
191
- "subscriptions": userstatus.get("subscriptions"),
192
- "sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits"),
193
- "extra_insight_credits": userstatus.get("extra_insight_credits")
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
- ####!!!!!!!!!! OPA call
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: {auth_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": auth_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 CustomJSONEncoder
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
- # Handle Pydantic models to exclude computed fields
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
- # Use the CustomJSONEncoder for serialization
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
- content,
45
+ json_safe_content,
43
46
  ensure_ascii=False,
44
47
  allow_nan=False,
45
48
  indent=None,
46
49
  separators=(",", ":"),
47
- default=CustomJSONEncoder().default
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: 10.1.1
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)