ipulse-shared-core-ftredge 16.0.1__tar.gz → 19.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.
- {ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-19.0.1}/PKG-INFO +2 -2
- ipulse_shared_core_ftredge-19.0.1/pyproject.toml +17 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/setup.py +2 -2
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/__init__.py +1 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +8 -5
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/exceptions/__init__.py +47 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +219 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -3
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +15 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +7 -6
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py +64 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +41 -7
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/user_status.py +44 -138
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/monitoring/__init__.py +5 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/monitoring/microservmon.py +483 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +25 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/base/__init__.py +12 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +520 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/services/cache_aware_firestore_service.py +44 -8
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/services/charging_service.py +1 -1
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +37 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/iam_management_operations.py +326 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +384 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_account_operations.py +479 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_auth_operations.py +305 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +651 -0
- ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +436 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +2 -2
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +14 -6
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -1
- ipulse_shared_core_ftredge-19.0.1/tests/test_cache_aware_service.py +270 -0
- ipulse_shared_core_ftredge-16.0.1/pyproject.toml +0 -3
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/__init__.py +0 -12
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -96
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -9
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -39
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +0 -18
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -249
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -140
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -240
- ipulse_shared_core_ftredge-16.0.1/tests/test_cache_aware_service.py +0 -234
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
- ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py → ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +1 -1
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/services/charging_processors.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
- {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.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: 19.0.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
|
|
@@ -13,7 +13,7 @@ Requires-Dist: pydantic[email]~=2.5
|
|
|
13
13
|
Requires-Dist: python-dateutil~=2.8
|
|
14
14
|
Requires-Dist: fastapi~=0.115.8
|
|
15
15
|
Requires-Dist: pytest
|
|
16
|
-
Requires-Dist: ipulse_shared_base_ftredge==
|
|
16
|
+
Requires-Dist: ipulse_shared_base_ftredge==7.2.0
|
|
17
17
|
Dynamic: author
|
|
18
18
|
Dynamic: classifier
|
|
19
19
|
Dynamic: home-page
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.pytest.ini_options]
|
|
6
|
+
asyncio_mode = "auto"
|
|
7
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
8
|
+
python_classes = ["Test*", "!TestModel"]
|
|
9
|
+
addopts = [
|
|
10
|
+
"-v",
|
|
11
|
+
"--tb=short",
|
|
12
|
+
"--strict-markers",
|
|
13
|
+
"--disable-warnings"
|
|
14
|
+
]
|
|
15
|
+
testpaths = ["tests"]
|
|
16
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
17
|
+
python_functions = ["test_*"]
|
|
@@ -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='19.0.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=[
|
|
@@ -12,7 +12,7 @@ setup(
|
|
|
12
12
|
'python-dateutil~=2.8',
|
|
13
13
|
'fastapi~=0.115.8',
|
|
14
14
|
'pytest',
|
|
15
|
-
'ipulse_shared_base_ftredge==
|
|
15
|
+
'ipulse_shared_base_ftredge==7.2.0',
|
|
16
16
|
],
|
|
17
17
|
author='Russlan Ramdowar',
|
|
18
18
|
description='Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# pylint: disable=missing-module-docstring
|
|
@@ -140,10 +140,12 @@ async def get_userstatus(
|
|
|
140
140
|
snapshot = await get_with_strict_timeout(user_ref, timeout)
|
|
141
141
|
|
|
142
142
|
if not snapshot.exists:
|
|
143
|
+
# Log at DEBUG level since this might be expected for new users
|
|
144
|
+
logger.debug(f"User status document not found for user {user_uid} (document: {userstatus_id})")
|
|
143
145
|
raise ResourceNotFoundError(
|
|
144
|
-
resource_type="
|
|
146
|
+
resource_type="authz_for_apis>userstatus",
|
|
145
147
|
resource_id=userstatus_id,
|
|
146
|
-
additional_info={"user_uid": user_uid}
|
|
148
|
+
additional_info={"user_uid": user_uid, "context": "authorization"}
|
|
147
149
|
)
|
|
148
150
|
|
|
149
151
|
status_data = snapshot.to_dict()
|
|
@@ -153,7 +155,10 @@ async def get_userstatus(
|
|
|
153
155
|
userstatus_cache.set(user_uid, status_data)
|
|
154
156
|
return status_data, cache_used
|
|
155
157
|
|
|
156
|
-
except
|
|
158
|
+
except ResourceNotFoundError:
|
|
159
|
+
# Re-raise ResourceNotFoundError as-is - don't wrap in ServiceError
|
|
160
|
+
raise
|
|
161
|
+
except (TimeoutError, FirestoreTimeoutError) as e:
|
|
157
162
|
logger.error(f"Timeout while fetching user status for {user_uid}: {str(e)}")
|
|
158
163
|
raise ServiceError(
|
|
159
164
|
operation="fetching user status for authz",
|
|
@@ -166,8 +171,6 @@ async def get_userstatus(
|
|
|
166
171
|
"timeout_seconds": timeout
|
|
167
172
|
}
|
|
168
173
|
)
|
|
169
|
-
except ResourceNotFoundError:
|
|
170
|
-
raise
|
|
171
174
|
except Exception as e:
|
|
172
175
|
logger.error(f"Error fetching user status for {user_uid}: {str(e)}")
|
|
173
176
|
raise ServiceError(
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception module for ipulse_shared_core_ftredge
|
|
3
|
+
|
|
4
|
+
This module centralizes all exceptions to prevent circular import dependencies.
|
|
5
|
+
All services import exceptions from here instead of from each other.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import all exceptions from submodules
|
|
9
|
+
from .base_exceptions import (
|
|
10
|
+
BaseServiceException,
|
|
11
|
+
ServiceError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
ResourceNotFoundError,
|
|
14
|
+
AuthorizationError
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .user_exceptions import (
|
|
18
|
+
UserCoreError,
|
|
19
|
+
UserCreationError,
|
|
20
|
+
UserDeletionError,
|
|
21
|
+
UserValidationError,
|
|
22
|
+
UserProfileError,
|
|
23
|
+
UserStatusError,
|
|
24
|
+
UserAuthError,
|
|
25
|
+
SubscriptionError,
|
|
26
|
+
IAMPermissionError
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Base exceptions
|
|
31
|
+
'BaseServiceException',
|
|
32
|
+
'ServiceError',
|
|
33
|
+
'ValidationError',
|
|
34
|
+
'ResourceNotFoundError',
|
|
35
|
+
'AuthorizationError',
|
|
36
|
+
|
|
37
|
+
# User-specific exceptions
|
|
38
|
+
'UserCoreError',
|
|
39
|
+
'UserCreationError',
|
|
40
|
+
'UserDeletionError',
|
|
41
|
+
'UserValidationError',
|
|
42
|
+
'UserProfileError',
|
|
43
|
+
'UserStatusError',
|
|
44
|
+
'UserAuthError',
|
|
45
|
+
'SubscriptionError',
|
|
46
|
+
'IAMPermissionError'
|
|
47
|
+
]
|
ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Custom exceptions for UserCoreService operations"""
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
from .base_exceptions import BaseServiceException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UserCoreError(BaseServiceException):
|
|
7
|
+
"""Base exception for UserCore operations"""
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
detail: str,
|
|
11
|
+
user_uid: Optional[str] = None,
|
|
12
|
+
operation: Optional[str] = None,
|
|
13
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
14
|
+
original_error: Optional[Exception] = None
|
|
15
|
+
):
|
|
16
|
+
super().__init__(
|
|
17
|
+
status_code=500,
|
|
18
|
+
detail=detail,
|
|
19
|
+
resource_type="UserCore",
|
|
20
|
+
resource_id=user_uid,
|
|
21
|
+
additional_info=additional_info,
|
|
22
|
+
original_error=original_error
|
|
23
|
+
)
|
|
24
|
+
self.operation = operation
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UserProfileError(UserCoreError):
|
|
28
|
+
"""Exception for UserProfile operations"""
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
detail: str,
|
|
32
|
+
user_uid: Optional[str] = None,
|
|
33
|
+
operation: Optional[str] = None,
|
|
34
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
35
|
+
original_error: Optional[Exception] = None
|
|
36
|
+
):
|
|
37
|
+
super().__init__(
|
|
38
|
+
detail=detail,
|
|
39
|
+
user_uid=user_uid,
|
|
40
|
+
operation=operation,
|
|
41
|
+
additional_info=additional_info,
|
|
42
|
+
original_error=original_error
|
|
43
|
+
)
|
|
44
|
+
self.resource_type = "UserProfile"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UserStatusError(UserCoreError):
|
|
48
|
+
"""Exception for UserStatus operations"""
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
detail: str,
|
|
52
|
+
user_uid: Optional[str] = None,
|
|
53
|
+
operation: Optional[str] = None,
|
|
54
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
55
|
+
original_error: Optional[Exception] = None
|
|
56
|
+
):
|
|
57
|
+
super().__init__(
|
|
58
|
+
detail=detail,
|
|
59
|
+
user_uid=user_uid,
|
|
60
|
+
operation=operation,
|
|
61
|
+
additional_info=additional_info,
|
|
62
|
+
original_error=original_error
|
|
63
|
+
)
|
|
64
|
+
self.resource_type = "UserStatus"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class UserAuthError(UserCoreError):
|
|
68
|
+
"""Exception for Firebase Auth operations"""
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
detail: str,
|
|
72
|
+
user_uid: Optional[str] = None,
|
|
73
|
+
operation: Optional[str] = None,
|
|
74
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
75
|
+
original_error: Optional[Exception] = None
|
|
76
|
+
):
|
|
77
|
+
super().__init__(
|
|
78
|
+
detail=detail,
|
|
79
|
+
user_uid=user_uid,
|
|
80
|
+
operation=operation,
|
|
81
|
+
additional_info=additional_info,
|
|
82
|
+
original_error=original_error
|
|
83
|
+
)
|
|
84
|
+
self.resource_type = "UserAuth"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SubscriptionError(UserCoreError):
|
|
88
|
+
"""Exception for subscription operations"""
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
detail: str,
|
|
92
|
+
user_uid: Optional[str] = None,
|
|
93
|
+
plan_id: Optional[str] = None,
|
|
94
|
+
operation: Optional[str] = None,
|
|
95
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
96
|
+
original_error: Optional[Exception] = None
|
|
97
|
+
):
|
|
98
|
+
additional_info = additional_info or {}
|
|
99
|
+
if plan_id:
|
|
100
|
+
additional_info['plan_id'] = plan_id
|
|
101
|
+
|
|
102
|
+
super().__init__(
|
|
103
|
+
detail=detail,
|
|
104
|
+
user_uid=user_uid,
|
|
105
|
+
operation=operation,
|
|
106
|
+
additional_info=additional_info,
|
|
107
|
+
original_error=original_error
|
|
108
|
+
)
|
|
109
|
+
self.resource_type = "Subscription"
|
|
110
|
+
self.plan_id = plan_id
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class IAMPermissionError(UserCoreError):
|
|
114
|
+
"""Exception for IAM permission operations"""
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
detail: str,
|
|
118
|
+
user_uid: Optional[str] = None,
|
|
119
|
+
domain: Optional[str] = None,
|
|
120
|
+
permission: Optional[str] = None,
|
|
121
|
+
operation: Optional[str] = None,
|
|
122
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
123
|
+
original_error: Optional[Exception] = None
|
|
124
|
+
):
|
|
125
|
+
additional_info = additional_info or {}
|
|
126
|
+
if domain:
|
|
127
|
+
additional_info['domain'] = domain
|
|
128
|
+
if permission:
|
|
129
|
+
additional_info['permission'] = permission
|
|
130
|
+
|
|
131
|
+
super().__init__(
|
|
132
|
+
detail=detail,
|
|
133
|
+
user_uid=user_uid,
|
|
134
|
+
operation=operation,
|
|
135
|
+
additional_info=additional_info,
|
|
136
|
+
original_error=original_error
|
|
137
|
+
)
|
|
138
|
+
self.resource_type = "IAMPermission"
|
|
139
|
+
self.domain = domain
|
|
140
|
+
self.permission = permission
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class UserCreationError(UserCoreError):
|
|
144
|
+
"""Exception for user creation operations"""
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
detail: str,
|
|
148
|
+
email: Optional[str] = None,
|
|
149
|
+
user_uid: Optional[str] = None,
|
|
150
|
+
failed_component: Optional[str] = None,
|
|
151
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
152
|
+
original_error: Optional[Exception] = None
|
|
153
|
+
):
|
|
154
|
+
additional_info = additional_info or {}
|
|
155
|
+
if email:
|
|
156
|
+
additional_info['email'] = email
|
|
157
|
+
if failed_component:
|
|
158
|
+
additional_info['failed_component'] = failed_component
|
|
159
|
+
|
|
160
|
+
super().__init__(
|
|
161
|
+
detail=detail,
|
|
162
|
+
user_uid=user_uid,
|
|
163
|
+
operation="create_user",
|
|
164
|
+
additional_info=additional_info,
|
|
165
|
+
original_error=original_error
|
|
166
|
+
)
|
|
167
|
+
self.resource_type = "UserCreation"
|
|
168
|
+
self.email = email
|
|
169
|
+
self.failed_component = failed_component
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class UserDeletionError(UserCoreError):
|
|
173
|
+
"""Exception for user deletion operations"""
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
detail: str,
|
|
177
|
+
user_uid: Optional[str] = None,
|
|
178
|
+
deletion_target: Optional[str] = None,
|
|
179
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
180
|
+
original_error: Optional[Exception] = None
|
|
181
|
+
):
|
|
182
|
+
additional_info = additional_info or {}
|
|
183
|
+
if deletion_target:
|
|
184
|
+
additional_info['deletion_target'] = deletion_target
|
|
185
|
+
|
|
186
|
+
super().__init__(
|
|
187
|
+
detail=detail,
|
|
188
|
+
user_uid=user_uid,
|
|
189
|
+
operation="delete_user",
|
|
190
|
+
additional_info=additional_info,
|
|
191
|
+
original_error=original_error
|
|
192
|
+
)
|
|
193
|
+
self.resource_type = "UserDeletion"
|
|
194
|
+
self.deletion_target = deletion_target
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class UserValidationError(UserCoreError):
|
|
198
|
+
"""Exception for user data validation"""
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
detail: str,
|
|
202
|
+
user_uid: Optional[str] = None,
|
|
203
|
+
validation_field: Optional[str] = None,
|
|
204
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
205
|
+
original_error: Optional[Exception] = None
|
|
206
|
+
):
|
|
207
|
+
additional_info = additional_info or {}
|
|
208
|
+
if validation_field:
|
|
209
|
+
additional_info['validation_field'] = validation_field
|
|
210
|
+
|
|
211
|
+
super().__init__(
|
|
212
|
+
detail=detail,
|
|
213
|
+
user_uid=user_uid,
|
|
214
|
+
operation="validate_user_core_data",
|
|
215
|
+
additional_info=additional_info,
|
|
216
|
+
original_error=original_error
|
|
217
|
+
)
|
|
218
|
+
self.resource_type = "UserValidation"
|
|
219
|
+
self.validation_field = validation_field
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
from .user_profile import UserProfile
|
|
2
2
|
from .subscription import Subscription
|
|
3
3
|
from .user_status import UserStatus, IAMUnitRefAssignment
|
|
4
|
-
from .user_profile_update import UserProfileUpdate
|
|
5
4
|
from .user_auth import UserAuth
|
|
6
|
-
from .
|
|
7
|
-
from .base_api_response import BaseAPIResponse , CustomJSONResponse
|
|
5
|
+
from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
|
|
8
6
|
from .base_data_model import BaseDataModel
|
|
9
7
|
|
|
10
8
|
|
|
@@ -11,6 +11,7 @@ T = TypeVar('T')
|
|
|
11
11
|
class BaseAPIResponse(BaseModel, Generic[T]):
|
|
12
12
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
13
13
|
success: bool
|
|
14
|
+
chargeable: bool = False # Added chargeable attribute
|
|
14
15
|
data: Optional[T] = None
|
|
15
16
|
message: Optional[str] = None
|
|
16
17
|
error: Optional[str] = None
|
|
@@ -19,6 +20,20 @@ class BaseAPIResponse(BaseModel, Generic[T]):
|
|
|
19
20
|
"timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
|
|
20
21
|
}
|
|
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
|
+
|
|
22
37
|
class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
|
|
23
38
|
total_count: int
|
|
24
39
|
page: int
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
2
|
from typing import Any
|
|
3
3
|
from typing import ClassVar
|
|
4
|
+
from typing import Optional, Dict
|
|
4
5
|
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
5
6
|
import dateutil.parser
|
|
6
7
|
|
|
7
8
|
class BaseDataModel(BaseModel):
|
|
8
9
|
"""Base model with common fields and configuration"""
|
|
9
|
-
model_config = ConfigDict(frozen=
|
|
10
|
+
model_config = ConfigDict(frozen=False, extra="forbid")
|
|
10
11
|
|
|
11
12
|
# Required class variables that must be defined in subclasses
|
|
12
13
|
VERSION: ClassVar[float]
|
|
@@ -17,13 +18,13 @@ class BaseDataModel(BaseModel):
|
|
|
17
18
|
schema_version: float = Field(
|
|
18
19
|
..., # Make this required
|
|
19
20
|
description="Version of this Class == version of DB Schema",
|
|
20
|
-
frozen=True
|
|
21
|
+
frozen=True # Keep schema version frozen for data integrity
|
|
21
22
|
)
|
|
22
23
|
|
|
23
|
-
# Audit fields
|
|
24
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)
|
|
25
|
-
created_by: str = Field(
|
|
26
|
-
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)
|
|
24
|
+
# Audit fields - now mutable for updates
|
|
25
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
26
|
+
created_by: str = Field(...)
|
|
27
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
27
28
|
updated_by: str = Field(...)
|
|
28
29
|
|
|
29
30
|
@classmethod
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any, List
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pydantic import BaseModel, Field, EmailStr, ConfigDict, field_validator
|
|
4
|
+
|
|
5
|
+
class UserAuth(BaseModel):
|
|
6
|
+
"""Comprehensive authentication model for user credentials and auth operations"""
|
|
7
|
+
model_config = ConfigDict(extra="forbid")
|
|
8
|
+
|
|
9
|
+
# Core authentication fields
|
|
10
|
+
email: EmailStr = Field(..., description="User's email address")
|
|
11
|
+
password: Optional[str] = Field(None, min_length=6, description="User's password (for creation/update only)")
|
|
12
|
+
|
|
13
|
+
# Firebase Auth specific fields
|
|
14
|
+
firebase_uid: Optional[str] = Field(None, description="Firebase Auth UID")
|
|
15
|
+
provider_id: str = Field(default="password", description="Authentication provider ID")
|
|
16
|
+
email_verified: bool = Field(default=False, description="Whether email is verified")
|
|
17
|
+
disabled: bool = Field(default=False, description="Whether user account is disabled")
|
|
18
|
+
|
|
19
|
+
# Multi-factor authentication
|
|
20
|
+
mfa_enabled: bool = Field(default=False, description="Whether MFA is enabled")
|
|
21
|
+
phone_number: Optional[str] = Field(None, description="Phone number for SMS MFA")
|
|
22
|
+
|
|
23
|
+
# Custom claims and metadata
|
|
24
|
+
custom_claims: Dict[str, Any] = Field(default_factory=dict, description="Firebase custom claims")
|
|
25
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional authentication metadata")
|
|
26
|
+
|
|
27
|
+
# Provider data
|
|
28
|
+
provider_data: List[Dict[str, Any]] = Field(default_factory=list, description="Provider-specific data")
|
|
29
|
+
|
|
30
|
+
# Account management
|
|
31
|
+
created_at: Optional[datetime] = Field(None, description="Account creation timestamp")
|
|
32
|
+
last_sign_in: Optional[datetime] = Field(None, description="Last sign-in timestamp")
|
|
33
|
+
last_refresh: Optional[datetime] = Field(None, description="Last token refresh timestamp")
|
|
34
|
+
|
|
35
|
+
# Password management
|
|
36
|
+
password_hash: Optional[str] = Field(None, description="Password hash (internal use only)")
|
|
37
|
+
password_salt: Optional[str] = Field(None, description="Password salt (internal use only)")
|
|
38
|
+
valid_since: Optional[datetime] = Field(None, description="Timestamp since when tokens are valid")
|
|
39
|
+
|
|
40
|
+
@field_validator('phone_number')
|
|
41
|
+
@classmethod
|
|
42
|
+
def validate_phone_number(cls, v: Optional[str]) -> Optional[str]:
|
|
43
|
+
"""Validate phone number format if provided"""
|
|
44
|
+
if v is None:
|
|
45
|
+
return v
|
|
46
|
+
# Basic E.164 format validation
|
|
47
|
+
if not v.startswith('+') or not v[1:].isdigit() or len(v) < 8 or len(v) > 16:
|
|
48
|
+
raise ValueError('Phone number must be in E.164 format (+1234567890)')
|
|
49
|
+
return v
|
|
50
|
+
|
|
51
|
+
@field_validator('custom_claims')
|
|
52
|
+
@classmethod
|
|
53
|
+
def validate_custom_claims(cls, v: Dict[str, Any]) -> Dict[str, Any]:
|
|
54
|
+
"""Validate custom claims don't contain reserved Firebase claims"""
|
|
55
|
+
reserved_claims = {
|
|
56
|
+
'iss', 'aud', 'auth_time', 'user_id', 'sub', 'iat', 'exp', 'email',
|
|
57
|
+
'email_verified', 'phone_number', 'name', 'picture', 'firebase'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for claim in v.keys():
|
|
61
|
+
if claim in reserved_claims:
|
|
62
|
+
raise ValueError(f'Custom claim "{claim}" is reserved by Firebase')
|
|
63
|
+
|
|
64
|
+
return v
|
|
@@ -4,6 +4,7 @@ from typing import Set, Optional, ClassVar, Dict, Any, List
|
|
|
4
4
|
from pydantic import EmailStr, Field, ConfigDict, field_validator, model_validator, computed_field
|
|
5
5
|
from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject
|
|
6
6
|
from .base_data_model import BaseDataModel
|
|
7
|
+
import re # Add re import
|
|
7
8
|
|
|
8
9
|
# ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
|
|
9
10
|
# CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
|
|
@@ -27,7 +28,7 @@ class UserProfile(BaseDataModel):
|
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
id: str = Field(
|
|
30
|
-
|
|
31
|
+
default="", # Will be auto-generated from user_uid if not provided
|
|
31
32
|
description=f"User Profile ID, format: {OBJ_REF}_user_uid"
|
|
32
33
|
)
|
|
33
34
|
|
|
@@ -42,7 +43,7 @@ class UserProfile(BaseDataModel):
|
|
|
42
43
|
description="Primary user type (e.g., customer, internal, admin, superadmin)"
|
|
43
44
|
)
|
|
44
45
|
|
|
45
|
-
# Renamed
|
|
46
|
+
# Renamed usertypes to secondary_usertypes
|
|
46
47
|
secondary_usertypes: List[str] = Field(
|
|
47
48
|
default_factory=list,
|
|
48
49
|
description="List of secondary user types"
|
|
@@ -71,11 +72,11 @@ class UserProfile(BaseDataModel):
|
|
|
71
72
|
)
|
|
72
73
|
|
|
73
74
|
# User-editable fields
|
|
74
|
-
username:
|
|
75
|
-
default=
|
|
76
|
-
max_length=
|
|
77
|
-
pattern="^[a-zA-Z0-9_
|
|
78
|
-
description="Username (public display name)"
|
|
75
|
+
username: str = Field(
|
|
76
|
+
default="", # Made optional with empty default - will be auto-generated
|
|
77
|
+
max_length=12, # Updated to 12 characters
|
|
78
|
+
pattern="^[a-zA-Z0-9_]+$", # Allow underscore
|
|
79
|
+
description="Username (public display name), max 12 chars, alphanumeric and underscore. Auto-generated from email if not provided."
|
|
79
80
|
)
|
|
80
81
|
dob: Optional[date] = Field(
|
|
81
82
|
default=None,
|
|
@@ -122,4 +123,37 @@ class UserProfile(BaseDataModel):
|
|
|
122
123
|
if 'user_uid' in data and data['user_uid']:
|
|
123
124
|
data['id'] = f"{cls.OBJ_REF}_{data['user_uid']}"
|
|
124
125
|
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
@model_validator(mode='before')
|
|
129
|
+
@classmethod
|
|
130
|
+
def populate_username(cls, data: Any) -> Any:
|
|
131
|
+
"""
|
|
132
|
+
Generates or sanitizes the username.
|
|
133
|
+
If username is provided and non-empty, it's sanitized and truncated to 10 chars.
|
|
134
|
+
If not provided or empty, it's generated from the email (part before '@'),
|
|
135
|
+
sanitized, and truncated to 10 chars.
|
|
136
|
+
If no email is available, generates a default username.
|
|
137
|
+
"""
|
|
138
|
+
if not isinstance(data, dict):
|
|
139
|
+
# Not a dict, perhaps an instance already, skip
|
|
140
|
+
return data
|
|
141
|
+
|
|
142
|
+
email = data.get('email')
|
|
143
|
+
username = data.get('username')
|
|
144
|
+
|
|
145
|
+
# Check if username is provided and non-empty
|
|
146
|
+
if username and isinstance(username, str) and username.strip():
|
|
147
|
+
# Sanitize and truncate provided username
|
|
148
|
+
sanitized_username = re.sub(r'[^a-zA-Z0-9_]', '', username)
|
|
149
|
+
data['username'] = sanitized_username[:12] if sanitized_username else "user"
|
|
150
|
+
elif email and isinstance(email, str):
|
|
151
|
+
# Generate from email
|
|
152
|
+
email_prefix = email.split('@')[0]
|
|
153
|
+
sanitized_prefix = re.sub(r'[^a-zA-Z0-9_]', '', email_prefix)
|
|
154
|
+
data['username'] = sanitized_prefix[:12] if sanitized_prefix else "user"
|
|
155
|
+
else:
|
|
156
|
+
# Fallback if no email or username provided
|
|
157
|
+
data['username'] = "user"
|
|
158
|
+
|
|
125
159
|
return data
|