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.

Files changed (59) hide show
  1. {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
  2. ipulse_shared_core_ftredge-19.0.1/pyproject.toml +17 -0
  3. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/setup.py +2 -2
  4. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/__init__.py +1 -0
  5. {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
  6. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/exceptions/__init__.py +47 -0
  7. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +219 -0
  8. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -3
  9. {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
  10. {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
  11. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py +64 -0
  12. {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
  13. {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
  14. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/monitoring/__init__.py +5 -0
  15. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/monitoring/microservmon.py +483 -0
  16. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +25 -0
  17. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/base/__init__.py +12 -0
  18. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +520 -0
  19. {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
  20. {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
  21. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +37 -0
  22. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/iam_management_operations.py +326 -0
  23. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +384 -0
  24. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_account_operations.py +479 -0
  25. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_auth_operations.py +305 -0
  26. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +651 -0
  27. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +436 -0
  28. {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
  29. {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
  30. {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
  31. ipulse_shared_core_ftredge-19.0.1/tests/test_cache_aware_service.py +270 -0
  32. ipulse_shared_core_ftredge-16.0.1/pyproject.toml +0 -3
  33. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/__init__.py +0 -12
  34. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -96
  35. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -9
  36. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -39
  37. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +0 -18
  38. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -249
  39. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -140
  40. ipulse_shared_core_ftredge-16.0.1/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -240
  41. ipulse_shared_core_ftredge-16.0.1/tests/test_cache_aware_service.py +0 -234
  42. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/LICENCE +0 -0
  43. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/README.md +0 -0
  44. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/setup.cfg +0 -0
  45. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
  46. {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
  47. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  48. {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
  49. {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
  50. {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
  51. 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
  52. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
  53. {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
  54. {ipulse_shared_core_ftredge-16.0.1 → ipulse_shared_core_ftredge-19.0.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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: 16.0.1
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==6.5.1
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='16.0.1',
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==6.5.1',
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="authorization userstatus",
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 TimeoutError as e:
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
+ ]
@@ -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 .organization_profile import OrganizationProfile
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=True, extra="forbid")
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), frozen=True)
25
- created_by: str = Field(..., frozen=True)
26
- updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
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
- ..., # Required, but will be auto-generated if not provided
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 user_types to secondary_usertypes
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: Optional[str] = Field(
75
- default=None,
76
- max_length=50,
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