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.
- {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
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/setup.py +1 -1
- ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/dependencies/__init__.py +3 -0
- {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
- ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py +67 -0
- {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
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +3 -1
- ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/models/base_api_response.py +29 -0
- ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/models/credit_api_response.py +26 -0
- 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
- {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
- {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
- ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/services/user/user_charging_operations.py +721 -0
- {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
- {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
- {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
- ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/utils/authz_credit_extraction.py +0 -0
- {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
- {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
- ipulse_shared_core_ftredge-25.1.1/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -1
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -0
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +0 -0
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-25.1.1 → ipulse_shared_core_ftredge-27.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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:
|
|
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='
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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]
|
ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/models/credit_api_response.py
ADDED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|