ipulse-shared-core-ftredge 18.0.1__tar.gz → 20.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 (60) hide show
  1. {ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-20.0.1}/PKG-INFO +1 -1
  2. ipulse_shared_core_ftredge-20.0.1/pyproject.toml +17 -0
  3. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/setup.py +1 -1
  4. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/__init__.py +1 -0
  5. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/exceptions/__init__.py +47 -0
  6. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +219 -0
  7. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/models/__init__.py +0 -2
  8. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +6 -6
  9. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py +64 -0
  10. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +41 -7
  11. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/models/user_status.py +44 -138
  12. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/monitoring/__init__.py +7 -0
  13. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/monitoring/microservmon.py +526 -0
  14. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +320 -0
  15. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +25 -0
  16. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/base/__init__.py +12 -0
  17. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +520 -0
  18. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/services/cache_aware_firestore_service.py +44 -8
  19. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/services/charging_service.py +1 -1
  20. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +37 -0
  21. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/iam_management_operations.py +326 -0
  22. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +384 -0
  23. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_account_operations.py +479 -0
  24. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_auth_operations.py +305 -0
  25. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +651 -0
  26. ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +436 -0
  27. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
  28. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +15 -6
  29. ipulse_shared_core_ftredge-20.0.1/tests/test_cache_aware_service.py +270 -0
  30. ipulse_shared_core_ftredge-18.0.1/pyproject.toml +0 -3
  31. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/__init__.py +0 -12
  32. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -96
  33. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -9
  34. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -39
  35. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +0 -18
  36. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -249
  37. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -140
  38. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -240
  39. ipulse_shared_core_ftredge-18.0.1/tests/test_cache_aware_service.py +0 -234
  40. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/LICENCE +0 -0
  41. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/README.md +0 -0
  42. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/setup.cfg +0 -0
  43. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
  44. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +0 -0
  45. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  46. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
  47. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  48. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +0 -0
  49. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
  50. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py → ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +1 -1
  51. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +0 -0
  52. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
  53. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/services/charging_processors.py +0 -0
  54. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  55. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
  56. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
  57. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  58. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
  59. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.0.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
  60. {ipulse_shared_core_ftredge-18.0.1 → ipulse_shared_core_ftredge-20.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: 18.0.1
3
+ Version: 20.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
@@ -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='18.0.1',
6
+ version='20.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=[
@@ -0,0 +1 @@
1
+ # pylint: disable=missing-module-docstring
@@ -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,9 +1,7 @@
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
5
  from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
8
6
  from .base_data_model import BaseDataModel
9
7
 
@@ -7,7 +7,7 @@ import dateutil.parser
7
7
 
8
8
  class BaseDataModel(BaseModel):
9
9
  """Base model with common fields and configuration"""
10
- model_config = ConfigDict(frozen=True, extra="forbid")
10
+ model_config = ConfigDict(frozen=False, extra="forbid")
11
11
 
12
12
  # Required class variables that must be defined in subclasses
13
13
  VERSION: ClassVar[float]
@@ -18,13 +18,13 @@ class BaseDataModel(BaseModel):
18
18
  schema_version: float = Field(
19
19
  ..., # Make this required
20
20
  description="Version of this Class == version of DB Schema",
21
- frozen=True
21
+ frozen=True # Keep schema version frozen for data integrity
22
22
  )
23
23
 
24
- # Audit fields
25
- created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
26
- created_by: str = Field(..., frozen=True)
27
- 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))
28
28
  updated_by: str = Field(...)
29
29
 
30
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