ipulse-shared-core-ftredge 25.1.1__tar.gz → 27.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.
Files changed (63) hide show
  1. {ipulse_shared_core_ftredge-25.1.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-27.1.1}/PKG-INFO +1 -1
  2. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/setup.py +1 -1
  3. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/dependencies/__init__.py +3 -0
  4. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -1
  5. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py +67 -0
  6. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +27 -19
  7. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +3 -1
  8. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/models/base_api_response.py +29 -0
  9. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/models/credit_api_response.py +26 -0
  10. ipulse_shared_core_ftredge-25.1.1/src/ipulse_shared_core_ftredge/models/base_api_response.py → ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/models/custom_json_response.py +4 -38
  11. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/charging_processors.py +13 -13
  12. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/__init__.py +1 -0
  13. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/services/user/user_charging_operations.py +721 -0
  14. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +123 -20
  15. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +42 -52
  16. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user_charging_service.py +18 -18
  17. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/utils/authz_credit_extraction.py +0 -0
  18. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
  19. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +5 -0
  20. ipulse_shared_core_ftredge-25.1.1/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -1
  21. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/LICENCE +0 -0
  22. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/README.md +0 -0
  23. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/pyproject.toml +0 -0
  24. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/setup.cfg +0 -0
  25. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
  26. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
  27. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +0 -0
  28. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  29. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
  30. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
  31. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +0 -0
  32. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +0 -0
  33. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +0 -0
  34. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/catalog/__init__.py +0 -0
  35. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +0 -0
  36. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/catalog/usertype.py +0 -0
  37. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/user/__init__.py +0 -0
  38. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/user/user_permissions.py +0 -0
  39. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/user/user_subscription.py +0 -0
  40. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/user/userauth.py +0 -0
  41. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/user/userprofile.py +0 -0
  42. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/user/userstatus.py +0 -0
  43. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -0
  44. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +0 -0
  45. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
  46. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/base/__init__.py +0 -0
  47. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +0 -0
  48. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/base/cache_aware_firestore_service.py +0 -0
  49. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/catalog/__init__.py +0 -0
  50. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +0 -0
  51. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +0 -0
  52. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +0 -0
  53. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +0 -0
  54. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/userauth_operations.py +0 -0
  55. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/userprofile_operations.py +0 -0
  56. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/user/userstatus_operations.py +0 -0
  57. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  58. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
  59. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
  60. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  61. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
  62. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
  63. {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/tests/test_shared_cache.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 25.1.1
3
+ Version: 27.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='25.1.1',
6
+ version='27.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=[
@@ -0,0 +1,3 @@
1
+ from .auth_firebase_token_validation import verify_firebase_token, AuthUser
2
+ from .authz_credit_extraction import extract_credits_from_authz_response
3
+ from .auth_protected_router import create_protected_router
@@ -67,7 +67,6 @@ async def verify_firebase_token(
67
67
  email = decoded_token.get('email')
68
68
  if not email:
69
69
  raise ValueError("Token must contain user email")
70
-
71
70
  user_auth = UserAuth(
72
71
  email=email,
73
72
  password=None, # No password for token-based auth
@@ -0,0 +1,67 @@
1
+ """
2
+ Utility functions for extracting credit information from authorization responses.
3
+
4
+ This module provides helper functions to extract credit information from OPA
5
+ authorization decisions, avoiding the need for complex dependency injection.
6
+ """
7
+ from typing import Optional, Dict, Any
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def extract_credits_from_authz_response(authz_info: Any) -> Optional[Dict[str, float]]:
14
+ """
15
+ Extract credit information from OPA authorization decision.
16
+
17
+ This function replaces the complex PreFetchedCredits dependency by directly
18
+ extracting credit information from the authorization response.
19
+
20
+ Args:
21
+ authz_info: Authorization information from OPA decision (typically AuthorizedRequest)
22
+
23
+ Returns:
24
+ Dictionary with credit information or None if not available:
25
+ {
26
+ "sbscrptn_based_insight_credits": float,
27
+ "extra_insight_credits": float
28
+ }
29
+ """
30
+ if not authz_info:
31
+ logger.debug("No authorization info provided for credit extraction")
32
+ return None
33
+
34
+ try:
35
+ # Handle dict-like authorization info (AuthorizedRequest typically converts to dict)
36
+ if isinstance(authz_info, dict) and 'opa_decision' in authz_info:
37
+ requestor_info = authz_info['opa_decision'].get("requestor_post_authz", {})
38
+ if requestor_info:
39
+ extracted_credits = {
40
+ "sbscrptn_based_insight_credits": float(requestor_info.get("sbscrptn_based_insight_credits", 0.0)),
41
+ "extra_insight_credits": float(requestor_info.get("extra_insight_credits", 0.0))
42
+ }
43
+ logger.debug("Successfully extracted credits from authz response: %s", extracted_credits)
44
+ return extracted_credits
45
+ else:
46
+ logger.debug("No requestor_post_authz found in OPA decision")
47
+ return None
48
+
49
+ # Handle object-like authorization info with attributes
50
+ elif hasattr(authz_info, 'opa_decision'):
51
+ opa_decision = getattr(authz_info, 'opa_decision', {})
52
+ if isinstance(opa_decision, dict):
53
+ requestor_info = opa_decision.get("requestor_post_authz", {})
54
+ if requestor_info:
55
+ extracted_credits = {
56
+ "sbscrptn_based_insight_credits": float(requestor_info.get("sbscrptn_based_insight_credits", 0.0)),
57
+ "extra_insight_credits": float(requestor_info.get("extra_insight_credits", 0.0))
58
+ }
59
+ logger.debug("Successfully extracted credits from authz response (object): %s", extracted_credits)
60
+ return extracted_credits
61
+
62
+ logger.debug("No valid OPA decision structure found in authz_info for credit extraction")
63
+ return None
64
+
65
+ except (AttributeError, KeyError, TypeError, ValueError) as e:
66
+ logger.warning("Failed to extract credits from authz response: %s", e)
67
+ return None
@@ -1,14 +1,13 @@
1
1
  import os
2
2
  import logging
3
- import asyncio
4
3
  from typing import Optional, Iterable, Dict, Any, List
5
4
  from datetime import datetime, timedelta, timezone
6
5
  import httpx
7
6
  from fastapi import HTTPException, Request
7
+ from ipulse_shared_base_ftredge import ApprovalStatus
8
8
  from ipulse_shared_core_ftredge.exceptions import ServiceError, ResourceNotFoundError
9
9
  from ipulse_shared_core_ftredge.models import UserStatus
10
10
  from ipulse_shared_core_ftredge.services import UserCoreService
11
- from ipulse_shared_base_ftredge import ApprovalStatus
12
11
 
13
12
  # Cache TTL constant
14
13
  USERSTATUS_CACHE_TTL = 60 # 60 seconds
@@ -63,12 +62,13 @@ class UserStatusCache:
63
62
  userstatus_cache = UserStatusCache()
64
63
 
65
64
  # Replace the logger dependency with a standard logger
66
- logger = logging.getLogger(__name__)
65
+ _module_logger = logging.getLogger(__name__)
67
66
 
68
67
  async def get_userstatus(
69
68
  user_uid: str,
70
69
  user_core_service: UserCoreService,
71
- force_fresh: bool = False
70
+ force_fresh: bool = False,
71
+ logger: Optional[logging.Logger] = None
72
72
  ) -> tuple[UserStatus, bool]:
73
73
  """
74
74
  Lightweight fetch of user status with caching.
@@ -78,10 +78,12 @@ async def get_userstatus(
78
78
  user_uid: User ID to fetch status for
79
79
  user_core_service: UserCoreService for data retrieval
80
80
  force_fresh: Whether to bypass cache
81
+ logger: Optional logger instance to use.
81
82
 
82
83
  Returns:
83
84
  Tuple of (UserStatus object, whether cache was used)
84
85
  """
86
+ log = logger if logger else _module_logger
85
87
  cache_used = False
86
88
 
87
89
  # Check cache first unless forced fresh
@@ -109,7 +111,7 @@ async def get_userstatus(
109
111
  raise ValueError(f"Expected UserStatus object or dict, got {type(status_obj)}")
110
112
 
111
113
  except Exception as e:
112
- logger.error(f"Error fetching user status via UserCoreService: {str(e)}")
114
+ log.error(f"Error fetching user status via UserCoreService: {str(e)}")
113
115
  raise ServiceError(
114
116
  operation="fetching user status for authz via UserCoreService",
115
117
  error=e,
@@ -134,11 +136,12 @@ def _validate_resource_fields(fields: Dict[str, Any]) -> List[str]:
134
136
  }
135
137
  return list(valid_fields.keys())
136
138
 
137
- async def extract_request_fields(request: Request) -> Optional[List[str]]:
139
+ async def extract_request_fields(request: Request, logger: Optional[logging.Logger] = None) -> Optional[List[str]]:
138
140
  """
139
141
  Extract fields from request body for both PATCH and POST methods.
140
142
  For GET and DELETE methods, return None as they typically don't have a body.
141
143
  """
144
+ log = logger if logger else _module_logger
142
145
  # Skip body extraction for GET and DELETE requests
143
146
  if request.method.upper() in ["GET", "DELETE", "HEAD", "OPTIONS"]:
144
147
  return None
@@ -161,7 +164,7 @@ async def extract_request_fields(request: Request) -> Optional[List[str]]:
161
164
  return None
162
165
 
163
166
  except Exception as e:
164
- logger.warning(f"Could not extract fields from request body: {str(e)}")
167
+ log.warning(f"Could not extract fields from request body: {str(e)}")
165
168
  return None # Return None instead of raising an error
166
169
 
167
170
  # Main authorization function with configurable timeout
@@ -169,6 +172,7 @@ async def authorizeAPIRequest(
169
172
  request: Request,
170
173
  user_core_service: UserCoreService, # UserCoreService instance for better integration
171
174
  request_resource_fields: Optional[Iterable[str]] = None,
175
+ logger: Optional[logging.Logger] = None
172
176
 
173
177
  ) -> Dict[str, Any]:
174
178
  """
@@ -179,6 +183,7 @@ async def authorizeAPIRequest(
179
183
  request: The incoming request
180
184
  user_core_service: UserCoreService instance for better integration
181
185
  request_resource_fields: Fields being accessed/modified in the request
186
+ logger: Optional logger instance to use.
182
187
 
183
188
  Returns:
184
189
  Authorization result containing decision details
@@ -186,17 +191,19 @@ async def authorizeAPIRequest(
186
191
  Raises:
187
192
  HTTPException: For authorization failures (403) or service errors (500)
188
193
  """
194
+ log = logger if logger else _module_logger
189
195
  opa_decision = None
190
196
  try:
191
197
  # Extract fields for both PATCH and POST if not provided
192
198
  if not request_resource_fields:
193
- request_resource_fields = await extract_request_fields(request)
199
+ request_resource_fields = await extract_request_fields(request, logger=log)
194
200
 
195
201
  # Extract request context and Firebase user claims
196
202
  firebase_user = request.state.user
203
+ log.debug(f"Firebase user: {firebase_user}")
197
204
  user_uid = firebase_user.get('uid')
198
205
  if not user_uid:
199
- logger.debug(f"Authorization denied for {request.method} {request.url.path}: No user UID found")
206
+ log.debug(f"Authorization denied for {request.method} {request.url.path}: No user UID found")
200
207
  raise HTTPException(
201
208
  status_code=403,
202
209
  detail="Not authorized to access this resource"
@@ -213,10 +220,11 @@ async def authorizeAPIRequest(
213
220
  user_uid=user_uid,
214
221
  user_core_service=user_core_service,
215
222
  force_fresh=force_fresh,
223
+ logger=log
216
224
  )
217
225
 
218
226
  # Perform comprehensive review and cleanup synchronously to ensure accurate auth data
219
- logger.info(f"Comprehensive review for userstatus : {user_status_obj} during authz")
227
+ log.debug(f"Comprehensive review for userstatus : {user_status_obj} during authz")
220
228
  if user_core_service:
221
229
  try:
222
230
  review_result = await user_core_service.review_and_clean_active_subscription_credits_and_permissions(
@@ -227,10 +235,10 @@ async def authorizeAPIRequest(
227
235
  clean_expired_permissions=True,
228
236
  review_credits=True
229
237
  )
230
- logger.info(f"Review result for userstatus : {review_result} during authz")
238
+ log.debug(f"Review result for userstatus : {review_result} during authz")
231
239
  # Refresh user status after comprehensive review if any actions were taken
232
240
  if review_result.get('actions_taken'):
233
- logger.info(f"Auth middleware performed comprehensive review for user {user_uid}: {review_result['actions_taken']}")
241
+ log.info(f"Authz middleware performed comprehensive review for user {user_uid}: {review_result['actions_taken']}")
234
242
  # Use the updated UserStatus returned from the review function
235
243
  if 'updated_userstatus' in review_result:
236
244
  user_status_obj = review_result['updated_userstatus']
@@ -240,7 +248,7 @@ async def authorizeAPIRequest(
240
248
  userstatus_cache.set(user_uid, user_status_obj)
241
249
 
242
250
  except Exception as e:
243
- logger.warning(f"Comprehensive review failed for user {user_uid} during auth: {str(e)}")
251
+ log.warning(f"Comprehensive review failed for user {user_uid} during auth: {str(e)}")
244
252
  # Continue with existing status if review fails
245
253
 
246
254
  # Get valid permissions after comprehensive review
@@ -298,17 +306,17 @@ async def authorizeAPIRequest(
298
306
  )
299
307
 
300
308
  if response.status_code != 200:
301
- logger.error(f"OPA authorization failed: {response.text}")
309
+ log.error(f"OPA authorization failed: {response.text}")
302
310
  raise HTTPException(
303
311
  status_code=500,
304
312
  detail="Authorization service error"
305
313
  )
306
314
 
307
315
  result = response.json()
308
-
316
+ log.debug(f"OPA response: {result}")
309
317
  # Handle OPA response format
310
318
  if "result" not in result:
311
- logger.warning("OPA response missing 'result' field")
319
+ log.warning("OPA response missing 'result' field")
312
320
  raise HTTPException(
313
321
  status_code=500,
314
322
  detail="Authorization service error: OPA response format unexpected"
@@ -319,14 +327,14 @@ async def authorizeAPIRequest(
319
327
 
320
328
  # Handle authorization denial
321
329
  if not allow:
322
- logger.debug(f"Authorization denied for {request.method} {request.url.path}")
330
+ log.debug(f"Authorization denied for {request.method} {request.url.path}")
323
331
  raise HTTPException(
324
332
  status_code=403,
325
333
  detail=f"Not authorized to {request.method} {request.url.path}"
326
334
  )
327
335
 
328
336
  except httpx.RequestError as e:
329
- logger.error(f"Failed to connect to OPA: {str(e)}")
337
+ log.error(f"Failed to connect to OPA: {str(e)}")
330
338
  raise HTTPException(
331
339
  status_code=500,
332
340
  detail="Authorization service temporarily unavailable"
@@ -345,7 +353,7 @@ async def authorizeAPIRequest(
345
353
  raise
346
354
  except Exception as e:
347
355
  # Only log unexpected errors at ERROR level
348
- logger.error(f"Unexpected error during authorization for {request.method} {request.url.path}: {str(e)}")
356
+ log.error(f"Unexpected error during authorization for {request.method} {request.url.path}: {str(e)}")
349
357
  raise HTTPException(
350
358
  status_code=500,
351
359
  detail="Internal authorization error"
@@ -1,4 +1,6 @@
1
1
  from .base_data_model import BaseDataModel
2
- from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
2
+ from .base_api_response import BaseAPIResponse, PaginatedAPIResponse
3
+ from .credit_api_response import CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
4
+ from .custom_json_response import CustomJSONResponse
3
5
  from .user import UserProfile, UserSubscription, UserStatus, UserAuth, UserAuthCreateNew, UserPermission
4
6
  from .catalog import SubscriptionPlan, ProrationMethod, PlanUpgradePath, UserType
@@ -0,0 +1,29 @@
1
+ """Core API response models."""
2
+ from typing import Generic, TypeVar, Optional, Any, Dict, List
3
+ import datetime as dt
4
+ from pydantic import BaseModel, ConfigDict
5
+
6
+
7
+ T = TypeVar('T')
8
+
9
+
10
+ class BaseAPIResponse(BaseModel, Generic[T]):
11
+ """Base API response model for all endpoints."""
12
+ model_config = ConfigDict(arbitrary_types_allowed=True)
13
+ success: bool
14
+ chargeable: bool = False # Added chargeable attribute
15
+ data: Optional[T] = None
16
+ message: Optional[str] = None
17
+ error: Optional[str] = None
18
+
19
+ metadata: Dict[str, Any] = {
20
+ "timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
21
+ }
22
+
23
+
24
+ class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
25
+ """API response for paginated data."""
26
+ total_count: int
27
+ page: int
28
+ page_size: int
29
+ items: List[T]
@@ -0,0 +1,26 @@
1
+ """Credit-related API response models."""
2
+ from typing import Generic, TypeVar, Optional
3
+ from pydantic import BaseModel
4
+ from .base_api_response import BaseAPIResponse
5
+
6
+ T = TypeVar('T')
7
+
8
+
9
+ class UserCreditBalance(BaseModel):
10
+ """User's current credit balance."""
11
+ sbscrptn_based_insight_credits: float
12
+ extra_insight_credits: float
13
+
14
+
15
+ class UpdatedUserCreditInfo(BaseModel):
16
+ """Information about credit charging attempt and results."""
17
+ charge_attempted: bool
18
+ charge_successful: bool
19
+ cost_incurred: float
20
+ items_processed_for_charge: int
21
+ user_balance: UserCreditBalance
22
+
23
+
24
+ class CreditChargeableAPIResponse(BaseAPIResponse[T], Generic[T]):
25
+ """API response for endpoints that may charge credits."""
26
+ updated_user_credit_info: Optional[UpdatedUserCreditInfo] = None
@@ -1,46 +1,12 @@
1
- from typing import Generic, TypeVar, Optional, Any, Dict, List
2
- import datetime as dt
1
+ """Custom JSON response handlers for FastAPI."""
3
2
  import json
4
- from pydantic import BaseModel, ConfigDict
5
3
  from fastapi.responses import JSONResponse
6
4
  from ipulse_shared_core_ftredge.utils.json_encoder import EnsureJSONEncoderCompatibility, convert_to_json_serializable
7
5
 
8
6
 
9
- T = TypeVar('T')
10
-
11
- class BaseAPIResponse(BaseModel, Generic[T]):
12
- model_config = ConfigDict(arbitrary_types_allowed=True)
13
- success: bool
14
- chargeable: bool = False # Added chargeable attribute
15
- data: Optional[T] = None
16
- message: Optional[str] = None
17
- error: Optional[str] = None
18
-
19
- metadata: Dict[str, Any] = {
20
- "timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
21
- }
22
-
23
- class UserCreditBalance(BaseModel):
24
- sbscrptn_based_insight_credits: float
25
- extra_insight_credits: float
26
-
27
- class UpdatedUserCreditInfo(BaseModel):
28
- charge_attempted: bool
29
- charge_successful: bool
30
- cost_incurred: float
31
- items_processed_for_charge: int
32
- user_balance: UserCreditBalance
33
-
34
- class CreditChargeableAPIResponse(BaseAPIResponse[T], Generic[T]):
35
- updated_user_credit_info: Optional[UpdatedUserCreditInfo] = None
36
-
37
- class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
38
- total_count: int
39
- page: int
40
- page_size: int
41
- items: List[T]
42
-
43
7
  class CustomJSONResponse(JSONResponse):
8
+ """Custom JSON response with enhanced serialization support."""
9
+
44
10
  def render(self, content) -> bytes:
45
11
  # First preprocess content with our utility function
46
12
  if isinstance(content, dict) and "data" in content and hasattr(content["data"], "model_dump"):
@@ -63,4 +29,4 @@ class CustomJSONResponse(JSONResponse):
63
29
  indent=None,
64
30
  separators=(",", ":"),
65
31
  cls=EnsureJSONEncoderCompatibility
66
- ).encode("utf-8")
32
+ ).encode("utf-8")
@@ -56,7 +56,7 @@ class ChargingProcessor:
56
56
  updated_user_credits = pre_fetched_credits
57
57
  elif self.user_charging_service: # Attempt to get current credits if not pre-fetched
58
58
  try:
59
- _, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
59
+ _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
60
60
  updated_user_credits = current_user_credits_from_verify
61
61
  except Exception: # pylint: disable=broad-except
62
62
  self.logger.warning(f"Could not fetch current credits for user {user_uid} for free item.")
@@ -77,7 +77,7 @@ class ChargingProcessor:
77
77
  updated_user_credits = pre_fetched_credits
78
78
  elif self.user_charging_service:
79
79
  try:
80
- _, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
80
+ _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
81
81
  updated_user_credits = current_user_credits_from_verify
82
82
  except Exception: # pylint: disable=broad-except
83
83
  self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass.")
@@ -101,7 +101,7 @@ class ChargingProcessor:
101
101
  }
102
102
 
103
103
  # Verify user has enough credits
104
- has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_credits(
104
+ has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(
105
105
  user_uid,
106
106
  credit_cost,
107
107
  pre_fetched_user_credits=pre_fetched_credits
@@ -121,7 +121,7 @@ class ChargingProcessor:
121
121
  }
122
122
 
123
123
  # Charge the user - this now returns (bool, Optional[Dict])
124
- charged, calculated_updated_credits = await self.user_charging_service.charge_credits_transaction(
124
+ charged, calculated_updated_credits = await self.user_charging_service.debit_credits_transaction(
125
125
  user_uid,
126
126
  credit_cost,
127
127
  operation_description
@@ -133,7 +133,7 @@ class ChargingProcessor:
133
133
 
134
134
 
135
135
  return {
136
- 'access_granted': True, # Access granted because verify_credits passed
136
+ 'access_granted': True, # Access granted because verify_enough_credits passed
137
137
  'charge_successful': charged,
138
138
  'cost': credit_cost,
139
139
  'reason': 'charged' if charged else 'charge_failed',
@@ -145,7 +145,7 @@ class ChargingProcessor:
145
145
  # Try to get current credits to return
146
146
  if self.user_charging_service:
147
147
  try:
148
- _, updated_user_credits = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
148
+ _, updated_user_credits = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
149
149
  except Exception: # pylint: disable=broad-except
150
150
  pass # Keep updated_user_credits as None
151
151
  ve.additional_info = ve.additional_info or {}
@@ -157,7 +157,7 @@ class ChargingProcessor:
157
157
  current_user_credits_on_error = None
158
158
  if self.user_charging_service:
159
159
  try:
160
- _, current_user_credits_on_error = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
160
+ _, current_user_credits_on_error = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
161
161
  except Exception: # pylint: disable=broad-except
162
162
  pass
163
163
  return {
@@ -230,7 +230,7 @@ class ChargingProcessor:
230
230
  updated_user_credits = pre_fetched_credits
231
231
  elif self.user_charging_service:
232
232
  try:
233
- _, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
233
+ _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
234
234
  updated_user_credits = current_user_credits_from_verify
235
235
 
236
236
  except Exception: # pylint: disable=broad-except
@@ -252,7 +252,7 @@ class ChargingProcessor:
252
252
  updated_user_credits = pre_fetched_credits
253
253
  elif self.user_charging_service:
254
254
  try:
255
- _, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
255
+ _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
256
256
  updated_user_credits = current_user_credits_from_verify
257
257
  except Exception: # pylint: disable=broad-except
258
258
  self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass for batch.")
@@ -280,7 +280,7 @@ class ChargingProcessor:
280
280
 
281
281
  try:
282
282
  # Verify user has enough credits for total cost
283
- has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_credits(
283
+ has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(
284
284
  user_uid,
285
285
  total_cost,
286
286
  pre_fetched_user_credits=pre_fetched_credits
@@ -300,7 +300,7 @@ class ChargingProcessor:
300
300
  }
301
301
 
302
302
  # Charge the user for all paid items
303
- charged, calculated_updated_credits = await self.user_charging_service.charge_credits_transaction(
303
+ charged, calculated_updated_credits = await self.user_charging_service.debit_credits_transaction(
304
304
  user_uid,
305
305
  total_cost,
306
306
  f"{operation_description} ({len(paid_items)} items, total cost: {total_cost})"
@@ -324,7 +324,7 @@ class ChargingProcessor:
324
324
  self.logger.error(f"Validation error during batch credit check for user {user_uid}: {str(ve)}")
325
325
  if self.user_charging_service:
326
326
  try:
327
- _, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
327
+ _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
328
328
  updated_user_credits = current_user_credits_from_verify
329
329
  except Exception: # pylint: disable=broad-except
330
330
  pass
@@ -336,7 +336,7 @@ class ChargingProcessor:
336
336
  current_credits_on_error = None
337
337
  if self.user_charging_service:
338
338
  try:
339
- _, current_credits_on_error = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
339
+ _, current_credits_on_error = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
340
340
  updated_user_credits = current_credits_on_error
341
341
  except Exception: # pylint: disable=broad-except
342
342
  pass
@@ -14,4 +14,5 @@ from .user_subscription_operations import UsersubscriptionOperations
14
14
  from .user_permissions_operations import UserpermissionsOperations
15
15
  from .userauth_operations import UserauthOperations
16
16
  from .user_multistep_operations import UsermultistepOperations
17
+ from .user_charging_operations import UserChargingOperations
17
18
  from .user_core_service import UserCoreService