ipulse-shared-core-ftredge 20.0.1__py3-none-any.whl → 22.1.1__py3-none-any.whl
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/cache/shared_cache.py +1 -2
- ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +4 -4
- ipulse_shared_core_ftredge/exceptions/base_exceptions.py +23 -0
- ipulse_shared_core_ftredge/models/__init__.py +3 -7
- ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
- ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
- ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +273 -0
- ipulse_shared_core_ftredge/models/catalog/usertype.py +170 -0
- ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
- ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
- ipulse_shared_core_ftredge/models/{subscription.py → user/user_subscription.py} +66 -20
- ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
- ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
- ipulse_shared_core_ftredge/models/user/userstatus.py +430 -0
- ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
- ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
- ipulse_shared_core_ftredge/services/__init__.py +11 -13
- ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
- ipulse_shared_core_ftredge/services/base/base_firestore_service.py +73 -14
- ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
- ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +273 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +307 -0
- ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
- ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
- ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +160 -0
- ipulse_shared_core_ftredge/services/user/user_core_service.py +423 -515
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +726 -0
- ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +484 -0
- ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
- ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
- ipulse_shared_core_ftredge/services/user/userstatus_operations.py +212 -0
- ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/METADATA +3 -4
- ipulse_shared_core_ftredge-22.1.1.dist-info/RECORD +51 -0
- ipulse_shared_core_ftredge/models/user_status.py +0 -495
- ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
- ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
- ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
- ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
- ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
- ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
- ipulse_shared_core_ftredge-20.0.1.dist-info/RECORD +0 -42
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Profile Operations - CRUD operations for UserProfile
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
from google.cloud import firestore
|
|
8
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
+
|
|
10
|
+
from ...models import UserProfile
|
|
11
|
+
from ...exceptions import ResourceNotFoundError, UserProfileError
|
|
12
|
+
from ..base import BaseFirestoreService
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UserprofileOperations:
|
|
16
|
+
"""
|
|
17
|
+
Handles CRUD operations for UserProfile documents
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
firestore_client: firestore.Client,
|
|
23
|
+
logger: Optional[logging.Logger] = None,
|
|
24
|
+
timeout: float = 10.0,
|
|
25
|
+
profile_collection: Optional[str] = None,
|
|
26
|
+
):
|
|
27
|
+
collection_name = profile_collection or UserProfile.get_collection_name()
|
|
28
|
+
self.db_service = BaseFirestoreService[UserProfile](
|
|
29
|
+
db=firestore_client,
|
|
30
|
+
collection_name=collection_name,
|
|
31
|
+
resource_type="UserProfile",
|
|
32
|
+
model_class=UserProfile,
|
|
33
|
+
logger=logger,
|
|
34
|
+
timeout=timeout
|
|
35
|
+
)
|
|
36
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
37
|
+
self.profile_collection_name = collection_name
|
|
38
|
+
|
|
39
|
+
# Archival configuration
|
|
40
|
+
self.archive_profile_on_delete = os.getenv('ARCHIVE_PROFILE_ON_DELETE', 'true').lower() == 'true'
|
|
41
|
+
self.archive_profile_collection_name = os.getenv(
|
|
42
|
+
'ARCHIVE_PROFILE_COLLECTION_NAME',
|
|
43
|
+
f"~archive_{self.profile_collection_name}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def get_userprofile(self, user_uid: str) -> Optional[UserProfile]:
|
|
47
|
+
"""Fetches a user profile from Firestore."""
|
|
48
|
+
self.logger.info(f"Fetching user profile for UID: {user_uid}")
|
|
49
|
+
try:
|
|
50
|
+
profile_data = await self.db_service.get_document(f"{UserProfile.OBJ_REF}_{user_uid}", convert_to_model=True)
|
|
51
|
+
if profile_data and isinstance(profile_data, UserProfile):
|
|
52
|
+
return profile_data
|
|
53
|
+
return None
|
|
54
|
+
except ResourceNotFoundError:
|
|
55
|
+
self.logger.warning(f"UserProfile not found for UID: {user_uid}")
|
|
56
|
+
return None
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.logger.error("Error fetching user profile for %s: %s", user_uid, e, exc_info=True)
|
|
59
|
+
raise UserProfileError(
|
|
60
|
+
detail=f"Failed to fetch user profile: {str(e)}",
|
|
61
|
+
user_uid=user_uid,
|
|
62
|
+
operation="get_userprofile",
|
|
63
|
+
original_error=e
|
|
64
|
+
) from e
|
|
65
|
+
|
|
66
|
+
async def create_userprofile(self, userprofile: UserProfile, creator_uid: Optional[str] = None) -> UserProfile:
|
|
67
|
+
"""Creates a new user profile in Firestore."""
|
|
68
|
+
self.logger.info(f"Creating user profile for UID: {userprofile.user_uid}")
|
|
69
|
+
try:
|
|
70
|
+
doc_id = f"{UserProfile.OBJ_REF}_{userprofile.user_uid}"
|
|
71
|
+
effective_creator_uid = creator_uid or userprofile.user_uid
|
|
72
|
+
await self.db_service.create_document(doc_id, userprofile.model_dump(exclude_none=True), creator_uid=effective_creator_uid)
|
|
73
|
+
self.logger.info(f"Successfully created user profile for UID: {userprofile.user_uid}")
|
|
74
|
+
return userprofile
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self.logger.error(f"Error creating user profile for {userprofile.user_uid}: {e}", exc_info=True)
|
|
77
|
+
raise UserProfileError(
|
|
78
|
+
detail=f"Failed to create user profile: {str(e)}",
|
|
79
|
+
user_uid=userprofile.user_uid,
|
|
80
|
+
operation="create_userprofile",
|
|
81
|
+
original_error=e
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def update_userprofile(self, user_uid: str, profile_data: Dict[str, Any], updater_uid: str) -> UserProfile:
|
|
85
|
+
"""Updates an existing user profile in Firestore."""
|
|
86
|
+
self.logger.info(f"Updating user profile for UID: {user_uid}")
|
|
87
|
+
try:
|
|
88
|
+
doc_id = f"{UserProfile.OBJ_REF}_{user_uid}"
|
|
89
|
+
await self.db_service.update_document(doc_id, profile_data, updater_uid)
|
|
90
|
+
updated_profile = await self.get_userprofile(user_uid)
|
|
91
|
+
if not updated_profile:
|
|
92
|
+
raise ResourceNotFoundError(
|
|
93
|
+
resource_type="UserProfile",
|
|
94
|
+
resource_id=doc_id
|
|
95
|
+
)
|
|
96
|
+
self.logger.info(f"Successfully updated user profile for UID: {user_uid}")
|
|
97
|
+
return updated_profile
|
|
98
|
+
except ResourceNotFoundError as e:
|
|
99
|
+
self.logger.error(f"Cannot update non-existent user profile for {user_uid}")
|
|
100
|
+
raise e
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self.logger.error(f"Error updating user profile for {user_uid}: {e}", exc_info=True)
|
|
103
|
+
raise UserProfileError(
|
|
104
|
+
detail=f"Failed to update user profile: {str(e)}",
|
|
105
|
+
user_uid=user_uid,
|
|
106
|
+
operation="update_userprofile",
|
|
107
|
+
original_error=e
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def delete_userprofile(self, user_uid: str, updater_uid: str = "system_deletion", archive: bool = True) -> bool:
|
|
111
|
+
"""Delete (archive and delete) user profile"""
|
|
112
|
+
profile_doc_id = f"{UserProfile.OBJ_REF}_{user_uid}"
|
|
113
|
+
should_archive = archive if archive is not None else self.archive_profile_on_delete
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Get profile data for archival
|
|
117
|
+
profile_data = await self.db_service.get_document(profile_doc_id, convert_to_model=False)
|
|
118
|
+
|
|
119
|
+
if profile_data:
|
|
120
|
+
# Ensure we have a dict for archival
|
|
121
|
+
profile_dict = profile_data if isinstance(profile_data, dict) else profile_data.__dict__
|
|
122
|
+
|
|
123
|
+
# Archive if enabled
|
|
124
|
+
if should_archive:
|
|
125
|
+
await self.db_service.archive_document(
|
|
126
|
+
document_data=profile_dict,
|
|
127
|
+
doc_id=profile_doc_id,
|
|
128
|
+
archive_collection=self.archive_profile_collection_name,
|
|
129
|
+
archived_by=updater_uid
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Delete the original document
|
|
133
|
+
await self.db_service.delete_document(profile_doc_id)
|
|
134
|
+
self.logger.info(f"Successfully deleted user profile: {profile_doc_id}")
|
|
135
|
+
return True
|
|
136
|
+
else:
|
|
137
|
+
self.logger.warning(f"User profile {profile_doc_id} not found for deletion")
|
|
138
|
+
return True # Consider non-existent as successfully deleted
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
self.logger.error(f"Failed to delete user profile {profile_doc_id}: {e}", exc_info=True)
|
|
142
|
+
raise UserProfileError(
|
|
143
|
+
detail=f"Failed to delete user profile: {str(e)}",
|
|
144
|
+
user_uid=user_uid,
|
|
145
|
+
operation="delete_userprofile",
|
|
146
|
+
original_error=e
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def userprofile_exists(self, user_uid: str) -> bool:
|
|
152
|
+
"""Check if a user profile exists."""
|
|
153
|
+
return await self.db_service.document_exists(f"{UserProfile.OBJ_REF}_{user_uid}")
|
|
154
|
+
|
|
155
|
+
async def validate_userprofile_data(
|
|
156
|
+
self,
|
|
157
|
+
profile_data: Optional[Dict[str, Any]] = None,
|
|
158
|
+
) -> tuple[bool, list[str]]:
|
|
159
|
+
"""Validate user profile data without creating documents"""
|
|
160
|
+
errors = []
|
|
161
|
+
if profile_data:
|
|
162
|
+
try:
|
|
163
|
+
UserProfile(**profile_data)
|
|
164
|
+
except PydanticValidationError as e:
|
|
165
|
+
errors.append(f"Profile validation error: {str(e)}")
|
|
166
|
+
return len(errors) == 0, errors
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Userstatus Operations - CRUD operations for Userstatus
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
|
|
8
|
+
from google.cloud import firestore
|
|
9
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
10
|
+
|
|
11
|
+
from ...models import UserStatus
|
|
12
|
+
from ...exceptions import ResourceNotFoundError, UserStatusError
|
|
13
|
+
from ..base import BaseFirestoreService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserstatusOperations:
|
|
17
|
+
"""
|
|
18
|
+
Handles CRUD operations for Userstatus documents
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
firestore_client: firestore.Client,
|
|
24
|
+
logger: Optional[logging.Logger] = None,
|
|
25
|
+
timeout: float = 10.0,
|
|
26
|
+
status_collection: Optional[str] = None
|
|
27
|
+
):
|
|
28
|
+
self.db = firestore_client
|
|
29
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
30
|
+
self.timeout = timeout
|
|
31
|
+
|
|
32
|
+
self.status_collection_name = status_collection or UserStatus.get_collection_name()
|
|
33
|
+
|
|
34
|
+
# Archival configuration
|
|
35
|
+
self.archive_userstatus_on_delete = os.getenv('ARCHIVE_USERSTATUS_ON_DELETE', 'true').lower() == 'true'
|
|
36
|
+
self.archive_userstatus_collection_name = os.getenv(
|
|
37
|
+
'ARCHIVE_USERSTATUS_COLLECTION_NAME',
|
|
38
|
+
"~archive_core_user_userstatuss"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Initialize DB service
|
|
42
|
+
self._status_db_service = BaseFirestoreService[UserStatus](
|
|
43
|
+
db=self.db,
|
|
44
|
+
collection_name=self.status_collection_name,
|
|
45
|
+
resource_type=UserStatus.OBJ_REF,
|
|
46
|
+
model_class=UserStatus,
|
|
47
|
+
logger=self.logger,
|
|
48
|
+
timeout=self.timeout
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def get_userstatus(self, user_uid: str, convert_to_model: bool = True) -> Optional[UserStatus]:
|
|
52
|
+
"""Retrieve a user status by UID"""
|
|
53
|
+
userstatus_id = f"{UserStatus.OBJ_REF}_{user_uid}"
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
userstatus = await self._status_db_service.get_document(
|
|
57
|
+
userstatus_id,
|
|
58
|
+
convert_to_model=convert_to_model
|
|
59
|
+
)
|
|
60
|
+
if userstatus:
|
|
61
|
+
self.logger.debug("Successfully retrieved user status for %s", user_uid)
|
|
62
|
+
# Always return a UserStatus model to match the return type
|
|
63
|
+
if isinstance(userstatus, dict):
|
|
64
|
+
return UserStatus(**userstatus)
|
|
65
|
+
return userstatus
|
|
66
|
+
else:
|
|
67
|
+
self.logger.debug("User status not found for %s", user_uid)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
except ResourceNotFoundError:
|
|
71
|
+
self.logger.debug("User status not found for %s", user_uid)
|
|
72
|
+
return None
|
|
73
|
+
except Exception as e:
|
|
74
|
+
self.logger.error("Failed to fetch user status for %s: %s", user_uid, str(e), exc_info=True)
|
|
75
|
+
raise UserStatusError(
|
|
76
|
+
detail=f"Failed to fetch user status: {str(e)}",
|
|
77
|
+
user_uid=user_uid,
|
|
78
|
+
operation="get_userstatus",
|
|
79
|
+
original_error=e
|
|
80
|
+
) from e
|
|
81
|
+
|
|
82
|
+
async def create_userstatus(self, userstatus: UserStatus, creator_uid: Optional[str] = None) -> UserStatus:
|
|
83
|
+
"""Create a new user status"""
|
|
84
|
+
self.logger.info(f"Creating user status for UID: {userstatus.user_uid}")
|
|
85
|
+
try:
|
|
86
|
+
doc_id = f"{UserStatus.OBJ_REF}_{userstatus.user_uid}"
|
|
87
|
+
effective_creator_uid = creator_uid or userstatus.user_uid
|
|
88
|
+
await self._status_db_service.create_document(doc_id, userstatus, effective_creator_uid)
|
|
89
|
+
self.logger.info("Successfully created user status for UID: %s", userstatus.user_uid)
|
|
90
|
+
return userstatus
|
|
91
|
+
except Exception as e:
|
|
92
|
+
self.logger.error("Error creating user status for %s: %s", userstatus.user_uid, e, exc_info=True)
|
|
93
|
+
raise UserStatusError(
|
|
94
|
+
detail=f"Failed to create user status: {str(e)}",
|
|
95
|
+
user_uid=userstatus.user_uid,
|
|
96
|
+
operation="create_userstatus",
|
|
97
|
+
original_error=e
|
|
98
|
+
) from e
|
|
99
|
+
|
|
100
|
+
async def update_userstatus(self, user_uid: str, status_data: Dict[str, Any], updater_uid: str) -> UserStatus:
|
|
101
|
+
"""Update a user status"""
|
|
102
|
+
userstatus_id = f"{UserStatus.OBJ_REF}_{user_uid}"
|
|
103
|
+
|
|
104
|
+
# Remove system fields that shouldn't be updated
|
|
105
|
+
update_data = status_data.copy()
|
|
106
|
+
update_data.pop('user_uid', None)
|
|
107
|
+
update_data.pop('id', None)
|
|
108
|
+
update_data.pop('created_at', None)
|
|
109
|
+
update_data.pop('created_by', None)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
updated_doc_dict = await self._status_db_service.update_document(
|
|
113
|
+
userstatus_id,
|
|
114
|
+
update_data,
|
|
115
|
+
updater_uid=updater_uid
|
|
116
|
+
)
|
|
117
|
+
self.logger.info("Userstatus for %s updated successfully by %s", user_uid, updater_uid)
|
|
118
|
+
return UserStatus(**updated_doc_dict)
|
|
119
|
+
except ResourceNotFoundError as exc:
|
|
120
|
+
raise UserStatusError(
|
|
121
|
+
detail="User status not found",
|
|
122
|
+
user_uid=user_uid,
|
|
123
|
+
operation="update_userstatus"
|
|
124
|
+
) from exc
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.logger.error("Error updating Userstatus for %s: %s", user_uid, str(e), exc_info=True)
|
|
127
|
+
raise UserStatusError(
|
|
128
|
+
detail=f"Failed to update user status: {str(e)}",
|
|
129
|
+
user_uid=user_uid,
|
|
130
|
+
operation="update_userstatus",
|
|
131
|
+
original_error=e
|
|
132
|
+
) from e
|
|
133
|
+
|
|
134
|
+
async def delete_userstatus(self, user_uid: str, updater_uid: str = "system_deletion", archive: Optional[bool] = True) -> bool:
|
|
135
|
+
"""Delete (archive and delete) user status"""
|
|
136
|
+
status_doc_id = f"{UserStatus.OBJ_REF}_{user_uid}"
|
|
137
|
+
should_archive = archive if archive is not None else self.archive_userstatus_on_delete
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Get status data for archival
|
|
141
|
+
status_data = await self._status_db_service.get_document(status_doc_id, convert_to_model=False)
|
|
142
|
+
|
|
143
|
+
if status_data:
|
|
144
|
+
# Ensure we have a dict for archival
|
|
145
|
+
status_dict = status_data if isinstance(status_data, dict) else status_data.__dict__
|
|
146
|
+
|
|
147
|
+
# Archive if enabled
|
|
148
|
+
if should_archive:
|
|
149
|
+
await self._status_db_service.archive_document(
|
|
150
|
+
document_data=status_dict,
|
|
151
|
+
doc_id=status_doc_id,
|
|
152
|
+
archive_collection=self.archive_userstatus_collection_name,
|
|
153
|
+
archived_by=updater_uid
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Delete the original document
|
|
157
|
+
await self._status_db_service.delete_document(status_doc_id)
|
|
158
|
+
self.logger.info("Successfully deleted user status: %s", status_doc_id)
|
|
159
|
+
return True
|
|
160
|
+
else:
|
|
161
|
+
self.logger.warning("User status %s not found for deletion", status_doc_id)
|
|
162
|
+
return True # Consider non-existent as successfully deleted
|
|
163
|
+
|
|
164
|
+
except ResourceNotFoundError:
|
|
165
|
+
self.logger.debug("User status %s not found for deletion (idempotent)", status_doc_id)
|
|
166
|
+
return True # Idempotent - already "deleted"
|
|
167
|
+
except Exception as e:
|
|
168
|
+
self.logger.error("Failed to delete user status %s: %s", status_doc_id, str(e), exc_info=True)
|
|
169
|
+
raise UserStatusError(
|
|
170
|
+
detail=f"Failed to delete user status: {str(e)}",
|
|
171
|
+
user_uid=user_uid,
|
|
172
|
+
operation="delete_userstatus",
|
|
173
|
+
original_error=e
|
|
174
|
+
) from e
|
|
175
|
+
|
|
176
|
+
async def validate_userstatus_data(
|
|
177
|
+
self,
|
|
178
|
+
status_data: Optional[Dict[str, Any]] = None
|
|
179
|
+
) -> tuple[bool, list[str]]:
|
|
180
|
+
"""Validate user status data without creating documents"""
|
|
181
|
+
errors = []
|
|
182
|
+
if status_data:
|
|
183
|
+
try:
|
|
184
|
+
UserStatus(**status_data)
|
|
185
|
+
except PydanticValidationError as e:
|
|
186
|
+
errors.append(f"Status validation error: {str(e)}")
|
|
187
|
+
return len(errors) == 0, errors
|
|
188
|
+
|
|
189
|
+
async def validate_and_cleanup_user_permissions(
|
|
190
|
+
self, user_uid: str, updater_uid: str, delete_expired: bool = True
|
|
191
|
+
) -> int:
|
|
192
|
+
"""Validate and clean up expired IAM permissions for a user."""
|
|
193
|
+
userstatus = await self.get_userstatus(user_uid)
|
|
194
|
+
if not userstatus:
|
|
195
|
+
self.logger.warning("Userstatus not found for %s, cannot validate permissions.", user_uid)
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
removed_count = userstatus.cleanup_expired_permissions()
|
|
199
|
+
|
|
200
|
+
if removed_count > 0 and delete_expired:
|
|
201
|
+
await self.update_userstatus(
|
|
202
|
+
user_uid,
|
|
203
|
+
userstatus.model_dump(exclude_none=True),
|
|
204
|
+
updater_uid=updater_uid
|
|
205
|
+
)
|
|
206
|
+
self.logger.info("Removed %d expired permissions for user %s.", removed_count, user_uid)
|
|
207
|
+
|
|
208
|
+
return removed_count
|
|
209
|
+
|
|
210
|
+
async def userstatus_exists(self, user_uid: str) -> bool:
|
|
211
|
+
"""Check if a user status exists."""
|
|
212
|
+
return await self._status_db_service.document_exists(f"{UserStatus.OBJ_REF}_{user_uid}")
|
|
@@ -4,12 +4,12 @@ from typing import Dict, Any, Optional, Tuple
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from google.cloud import firestore
|
|
6
6
|
from ipulse_shared_core_ftredge.exceptions import ServiceError, ResourceNotFoundError, ValidationError
|
|
7
|
-
from ipulse_shared_core_ftredge.models
|
|
7
|
+
from ipulse_shared_core_ftredge.models import UserStatus
|
|
8
8
|
|
|
9
9
|
# Default Firestore timeout if not provided by the consuming application
|
|
10
10
|
DEFAULT_FIRESTORE_TIMEOUT = 15.0
|
|
11
11
|
|
|
12
|
-
class
|
|
12
|
+
class UserChargingService:
|
|
13
13
|
"""
|
|
14
14
|
Service class for charging operations.
|
|
15
15
|
Designed to be project-agnostic and directly uses UserStatus model constants.
|
|
@@ -32,13 +32,13 @@ class ChargingService:
|
|
|
32
32
|
self.db = db
|
|
33
33
|
# Use UserStatus constants directly
|
|
34
34
|
self.users_status_collection_name = UserStatus.COLLECTION_NAME
|
|
35
|
-
self.
|
|
35
|
+
self.userstatus_doc_prefix = f"{UserStatus.OBJ_REF}_" # Append underscore to OBJ_REF
|
|
36
36
|
self.logger = logger or logging.getLogger(__name__)
|
|
37
37
|
self.timeout = firestore_timeout
|
|
38
38
|
|
|
39
39
|
self.logger.info(
|
|
40
40
|
f"ChargingService initialized using UserStatus constants. Collection: {self.users_status_collection_name}, "
|
|
41
|
-
f"Doc Prefix: {self.
|
|
41
|
+
f"Doc Prefix: {self.userstatus_doc_prefix}, Timeout: {self.timeout}s"
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
async def verify_credits(
|
|
@@ -57,7 +57,7 @@ class ChargingService:
|
|
|
57
57
|
(keys: 'sbscrptn_based_insight_credits', 'extra_insight_credits')
|
|
58
58
|
|
|
59
59
|
Returns:
|
|
60
|
-
Tuple of (has_enough_credits,
|
|
60
|
+
Tuple of (has_enough_credits, userstatus_data) where userstatus_data
|
|
61
61
|
will be a dict with keys 'sbscrptn_based_insight_credits' and 'extra_insight_credits'.
|
|
62
62
|
|
|
63
63
|
Raises:
|
|
@@ -174,7 +174,7 @@ class ChargingService:
|
|
|
174
174
|
|
|
175
175
|
|
|
176
176
|
try:
|
|
177
|
-
userstatus_id = f"{self.
|
|
177
|
+
userstatus_id = f"{self.userstatus_doc_prefix}{user_uid}"
|
|
178
178
|
user_ref = self.db.collection(self.users_status_collection_name).document(userstatus_id)
|
|
179
179
|
|
|
180
180
|
transaction = self.db.transaction()
|
|
@@ -277,7 +277,7 @@ class ChargingService:
|
|
|
277
277
|
async def _get_userstatus(self, user_uid: str) -> Dict[str, Any]:
|
|
278
278
|
"""Get a user's status document."""
|
|
279
279
|
try:
|
|
280
|
-
userstatus_id = f"{self.
|
|
280
|
+
userstatus_id = f"{self.userstatus_doc_prefix}{user_uid}"
|
|
281
281
|
doc_ref = self.db.collection(self.users_status_collection_name).document(userstatus_id)
|
|
282
282
|
|
|
283
283
|
# Using the timeout value set during initialization
|
|
@@ -285,7 +285,7 @@ class ChargingService:
|
|
|
285
285
|
|
|
286
286
|
if not doc.exists:
|
|
287
287
|
raise ResourceNotFoundError(
|
|
288
|
-
resource_type="
|
|
288
|
+
resource_type="userstatus", # Generic resource type
|
|
289
289
|
resource_id=userstatus_id,
|
|
290
290
|
additional_info={"collection": self.users_status_collection_name}
|
|
291
291
|
)
|
|
@@ -299,7 +299,7 @@ class ChargingService:
|
|
|
299
299
|
raise ServiceError(
|
|
300
300
|
operation="getting user status",
|
|
301
301
|
error=e,
|
|
302
|
-
resource_type="
|
|
302
|
+
resource_type="userstatus",
|
|
303
303
|
resource_id=user_uid,
|
|
304
304
|
additional_info={"collection": self.users_status_collection_name}
|
|
305
305
|
) from e
|
{ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/METADATA
RENAMED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 22.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
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
11
|
License-File: LICENCE
|
|
12
12
|
Requires-Dist: pydantic[email]~=2.5
|
|
13
13
|
Requires-Dist: python-dateutil~=2.8
|
|
14
14
|
Requires-Dist: fastapi~=0.115.8
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist: ipulse_shared_base_ftredge==7.2.0
|
|
15
|
+
Requires-Dist: ipulse_shared_base_ftredge==10.2.1
|
|
17
16
|
Dynamic: author
|
|
18
17
|
Dynamic: classifier
|
|
19
18
|
Dynamic: home-page
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
ipulse_shared_core_ftredge/__init__.py,sha256=-KbdF_YW8pgf7pVv9qh_cA1xrNm_B9zigHYDo7ZA4eU,42
|
|
2
|
+
ipulse_shared_core_ftredge/cache/__init__.py,sha256=i2fPojmZiBwAoY5ovnnnME9USl4bi8MRPYkAgEfACfI,136
|
|
3
|
+
ipulse_shared_core_ftredge/cache/shared_cache.py,sha256=BDJtkTsdfmVjKaUkbBXOhJ2Oib7Li0UCsPjWX7FLIPU,12940
|
|
4
|
+
ipulse_shared_core_ftredge/dependencies/__init__.py,sha256=HGsR8HUguKTfjz_BorCILS4izX8CAjG-apE0kIPE0Yo,68
|
|
5
|
+
ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py,sha256=EFWyhoVOI0tGYOWqN5St4JNIy4cMwpxeBhKdjOwEfbg,1888
|
|
6
|
+
ipulse_shared_core_ftredge/dependencies/auth_protected_router.py,sha256=em5D5tE7OkgZmuCtYCKuUAnIZCgRJhCF8Ye5QmtGWlk,1807
|
|
7
|
+
ipulse_shared_core_ftredge/dependencies/authz_for_apis.py,sha256=TnJ2ubGUZRxRa6v-OLC2uCLzaoCuX1GkXSaFJMaYg0s,15793
|
|
8
|
+
ipulse_shared_core_ftredge/dependencies/firestore_client.py,sha256=VbTb121nsc9EZPd1RDEsHBLW5pIiVw6Wdo2JFL4afMg,714
|
|
9
|
+
ipulse_shared_core_ftredge/exceptions/__init__.py,sha256=Cb_RsIie4DbT_NLwFVwjw4riDKsNNRQEuAvHvYa-Zco,1038
|
|
10
|
+
ipulse_shared_core_ftredge/exceptions/base_exceptions.py,sha256=RuihEpgXdUIR2XEXaP6aJnepePbvZ2RZYQwcKfatGPw,4981
|
|
11
|
+
ipulse_shared_core_ftredge/exceptions/user_exceptions.py,sha256=I-nm21MKrUYEoybpRODeYNzc184HfgHvRZQm_xux4VY,6824
|
|
12
|
+
ipulse_shared_core_ftredge/models/__init__.py,sha256=oaBL_BZWd7hIDu2K7yxtVKxtkOD9UF9r9_V2ZIPQ8Yk,350
|
|
13
|
+
ipulse_shared_core_ftredge/models/base_api_response.py,sha256=OwuWI2PsMSLDkFt643u35ZhW5AHFEMMAGnGprmUO0fA,2380
|
|
14
|
+
ipulse_shared_core_ftredge/models/base_data_model.py,sha256=GZ7KTT5FanHTgvmaHHTxawzAJtuixkbyb-SuL-mjWys,2193
|
|
15
|
+
ipulse_shared_core_ftredge/models/catalog/__init__.py,sha256=9oKJ74_mTtmj-0iDnRBiPI8m8QJ2J9wvx4ZWaZw3zRk,208
|
|
16
|
+
ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py,sha256=5q1yUwy_755UNt7FZ4Ez-1njF0APSyzmiLbhGP1f2g0,9069
|
|
17
|
+
ipulse_shared_core_ftredge/models/catalog/usertype.py,sha256=W42bis9l6_FL5Q3HY8pqxs1b0gkcIGgGYpLTOU6fYxE,6319
|
|
18
|
+
ipulse_shared_core_ftredge/models/user/__init__.py,sha256=TheOLldY6v-OK9i-A5mQNIxjHhBFpuOJ43mi-swcN_o,196
|
|
19
|
+
ipulse_shared_core_ftredge/models/user/user_permissions.py,sha256=CUWDBPLPmKN3o43BTZAt0zJvm_ekjJA46iV6rVNp-oc,2411
|
|
20
|
+
ipulse_shared_core_ftredge/models/user/user_subscription.py,sha256=f0EFPtIQU_KKB3LO1MKZYraaxSdJp-FqeZDPKs2VP6E,8587
|
|
21
|
+
ipulse_shared_core_ftredge/models/user/userauth.py,sha256=PbS-XSLxDl1feskI0iCziGvmMiLuF8o_ZTspAx1B0j0,3679
|
|
22
|
+
ipulse_shared_core_ftredge/models/user/userprofile.py,sha256=7VbE4qiKpDxZsNTk-IJKA32QxW0JOo8KWPkj8h9J2-Y,6945
|
|
23
|
+
ipulse_shared_core_ftredge/models/user/userstatus.py,sha256=NpSQWlbCnFRQ_Gnx7WLkyt3dKgRDf4-MRrJU5VYF7DA,16748
|
|
24
|
+
ipulse_shared_core_ftredge/monitoring/__init__.py,sha256=gUoJjT0wj-cQYnMWheWbh1mmRHmaeojmnBZTj7KPNus,61
|
|
25
|
+
ipulse_shared_core_ftredge/monitoring/tracemon.py,sha256=Trku0qrwWvEcvKsBWiYokd_G3fcH-5uP2wRVgcgIz_k,11596
|
|
26
|
+
ipulse_shared_core_ftredge/services/__init__.py,sha256=9AkMLCHNswhuNbQuJZaEVz4zt4F84PxfJLyU_bYk4Js,565
|
|
27
|
+
ipulse_shared_core_ftredge/services/charging_processors.py,sha256=9Re24dyXdjKYbqwx6uNLu3JBzIaw87TAV7Oe__M1QnA,16308
|
|
28
|
+
ipulse_shared_core_ftredge/services/user_charging_service.py,sha256=C3wMfgBXOz4RM1RLc7up2_pIPAnZv8ZYu-lLrkofTmc,14625
|
|
29
|
+
ipulse_shared_core_ftredge/services/base/__init__.py,sha256=zhyrHQMM0cLJr4spk2b6VsgJXuWBy7hUBzhrq_Seg9k,389
|
|
30
|
+
ipulse_shared_core_ftredge/services/base/base_firestore_service.py,sha256=xXmPfDEgBa4trWMJz1UBJRDLPxi8TtG2jDFbo_B28pw,19313
|
|
31
|
+
ipulse_shared_core_ftredge/services/base/cache_aware_firestore_service.py,sha256=ya5Asff9BQodYnJVAw6M_Pm8WtVRPpEK7izFlZ2MyjA,10016
|
|
32
|
+
ipulse_shared_core_ftredge/services/catalog/__init__.py,sha256=ctc2nDGwsW_Ji4lk9pys3oyNwR_V-gHSbSHawym5fKQ,385
|
|
33
|
+
ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py,sha256=mbwCC1RnIZEav5i5sRGT15nxQJF0qMVA2LT8BAjmJuY,9264
|
|
34
|
+
ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py,sha256=pZdsRt9V96NcYTRremTf76fDL126lAiraxGs936i0aU,9959
|
|
35
|
+
ipulse_shared_core_ftredge/services/user/__init__.py,sha256=jmkD5XzAmaD8QV2UsgB5xynGcfsXliWtRtN2pt6kzbA,884
|
|
36
|
+
ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py,sha256=7N-MWpCwzWCjIuIFXGv1sIjJJcU9M92nWOfRyMEuHbE,5430
|
|
37
|
+
ipulse_shared_core_ftredge/services/user/user_core_service.py,sha256=9nhFgFVPp188JMiZYINLwyHGtUYqtbHZhIR6cQMhj00,23648
|
|
38
|
+
ipulse_shared_core_ftredge/services/user/user_multistep_operations.py,sha256=hQ1-BygC2Cxin6AUsEVgEWfZg035v5yn__sAoPzy0Us,35543
|
|
39
|
+
ipulse_shared_core_ftredge/services/user/user_permissions_operations.py,sha256=FByszIWo-qooLVXFTw0tGLWksIJEqHUPc_ZGwue0_pM,15753
|
|
40
|
+
ipulse_shared_core_ftredge/services/user/user_subscription_operations.py,sha256=h5jz4jvnwocDIwHa5VUX3_u4Qzrkt5A1s06aL5Bafm0,21266
|
|
41
|
+
ipulse_shared_core_ftredge/services/user/userauth_operations.py,sha256=9l2uBAcAxbUnilK8MZ7IlHzaGiaPuqx7nIC51mAyR9w,36120
|
|
42
|
+
ipulse_shared_core_ftredge/services/user/userprofile_operations.py,sha256=_qyIEAQYCTV-subgP-5naMs_26apCpauomE6qmCCVWs,7333
|
|
43
|
+
ipulse_shared_core_ftredge/services/user/userstatus_operations.py,sha256=Gz2P4toJNs3ZMpGBvmN0HfiFqonRv6G3jTKkDz7mjuA,8927
|
|
44
|
+
ipulse_shared_core_ftredge/utils/__init__.py,sha256=JnxUb8I2MRjJC7rBPXSrpwBIQDEOku5O9JsiTi3oun8,56
|
|
45
|
+
ipulse_shared_core_ftredge/utils/custom_json_encoder.py,sha256=DblQLD0KOSNDyQ58wQRogBrShIXzPIZUw_oGOBATnJY,1366
|
|
46
|
+
ipulse_shared_core_ftredge/utils/json_encoder.py,sha256=QkcaFneVv3-q-s__Dz4OiUWYnM6jgHDJrDMdPv09RCA,2093
|
|
47
|
+
ipulse_shared_core_ftredge-22.1.1.dist-info/licenses/LICENCE,sha256=YBtYAXNqCCOo9Mr2hfkbSPAM9CeAr2j1VZBSwQTrNwE,1060
|
|
48
|
+
ipulse_shared_core_ftredge-22.1.1.dist-info/METADATA,sha256=Q-CLpF1xpkRoIMZ5ab17rGskMnkPdA6M04e0qj8yqHI,782
|
|
49
|
+
ipulse_shared_core_ftredge-22.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
50
|
+
ipulse_shared_core_ftredge-22.1.1.dist-info/top_level.txt,sha256=8sgYrptpexkA_6_HyGvho26cVFH9kmtGvaK8tHbsGHk,27
|
|
51
|
+
ipulse_shared_core_ftredge-22.1.1.dist-info/RECORD,,
|