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.

Files changed (47) hide show
  1. ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
  2. ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +4 -4
  3. ipulse_shared_core_ftredge/exceptions/base_exceptions.py +23 -0
  4. ipulse_shared_core_ftredge/models/__init__.py +3 -7
  5. ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
  6. ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
  7. ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +273 -0
  8. ipulse_shared_core_ftredge/models/catalog/usertype.py +170 -0
  9. ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
  10. ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
  11. ipulse_shared_core_ftredge/models/{subscription.py → user/user_subscription.py} +66 -20
  12. ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
  13. ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
  14. ipulse_shared_core_ftredge/models/user/userstatus.py +430 -0
  15. ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
  16. ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
  17. ipulse_shared_core_ftredge/services/__init__.py +11 -13
  18. ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
  19. ipulse_shared_core_ftredge/services/base/base_firestore_service.py +73 -14
  20. ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
  21. ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
  22. ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +273 -0
  23. ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +307 -0
  24. ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
  25. ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
  26. ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +160 -0
  27. ipulse_shared_core_ftredge/services/user/user_core_service.py +423 -515
  28. ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +726 -0
  29. ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
  30. ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +484 -0
  31. ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
  32. ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
  33. ipulse_shared_core_ftredge/services/user/userstatus_operations.py +212 -0
  34. ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
  35. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/METADATA +3 -4
  36. ipulse_shared_core_ftredge-22.1.1.dist-info/RECORD +51 -0
  37. ipulse_shared_core_ftredge/models/user_status.py +0 -495
  38. ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
  39. ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
  40. ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
  41. ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
  42. ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
  43. ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
  44. ipulse_shared_core_ftredge-20.0.1.dist-info/RECORD +0 -42
  45. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/WHEEL +0 -0
  46. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/licenses/LICENCE +0 -0
  47. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,20 @@
1
1
  from datetime import datetime, timezone
2
2
  from dateutil.relativedelta import relativedelta
3
3
  import uuid
4
- from typing import Set, Optional, ClassVar, Dict, Any, List, Union
5
- from pydantic import Field, ConfigDict
6
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject, SubscriptionPlan, SubscriptionStatus
7
- from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnitType
8
- from .base_data_model import BaseDataModel
4
+ from typing import Optional, ClassVar, Dict, Any, List
5
+ from pydantic import Field, ConfigDict, model_validator
6
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, SubscriptionPlanName, SubscriptionStatus
7
+ from ..base_data_model import BaseDataModel
8
+ from .user_permissions import UserPermission
9
9
  # ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
10
10
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
11
11
 
12
12
 
13
- DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlan.FREE
13
+ DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlanName.FREE_SUBSCRIPTION
14
14
  DEFAULT_SUBSCRIPTION_STATUS = SubscriptionStatus.ACTIVE
15
15
 
16
16
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
17
- class Subscription(BaseDataModel):
17
+ class UserSubscription(BaseDataModel):
18
18
  """
19
19
  Represents a single subscription cycle with enhanced flexibility and tracking.
20
20
  """
@@ -22,7 +22,7 @@ class Subscription(BaseDataModel):
22
22
  model_config = ConfigDict(frozen=True, extra="forbid")
23
23
 
24
24
  VERSION: ClassVar[float] = 3.0 # Incremented version for direct fields instead of computed
25
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.SUBSCRIPTION.name))
25
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.SUBSCRIPTION))
26
26
  OBJ_REF: ClassVar[str] = "subscription"
27
27
 
28
28
  # System-managed fields (read-only)
@@ -33,13 +33,13 @@ class Subscription(BaseDataModel):
33
33
  )
34
34
 
35
35
  # Unique identifier for this specific subscription instance - now auto-generated
36
- uuid: str = Field(
37
- default_factory=lambda: str(uuid.uuid4()),
36
+ id: Optional[str] = Field(
37
+ default=None, # Will be auto-generated using UUID if not provided
38
38
  description="Unique identifier for this subscription instance"
39
39
  )
40
40
 
41
41
  # Plan identification
42
- plan_name: SubscriptionPlan = Field(
42
+ plan_name: SubscriptionPlanName = Field(
43
43
  ..., # Required field, no default
44
44
  description="Subscription Plan Name"
45
45
  )
@@ -61,10 +61,10 @@ class Subscription(BaseDataModel):
61
61
  description="Subscription Cycle Start Date"
62
62
  )
63
63
 
64
- # Direct field instead of computed
65
- cycle_end_date: datetime = Field(
66
- ..., # Required field, no default
67
- description="Subscription Cycle End Date"
64
+ # Direct field instead of computed - will be auto-calculated
65
+ cycle_end_date: Optional[datetime] = Field(
66
+ default=None,
67
+ description="Subscription Cycle End Date (auto-calculated if not provided)"
68
68
  )
69
69
 
70
70
  # Fields for cycle calculation
@@ -89,14 +89,14 @@ class Subscription(BaseDataModel):
89
89
  description="Subscription Status (active, trial, pending_confirmation, etc.)"
90
90
  )
91
91
 
92
- # IAM permissions structure
93
- default_iam_domain_permissions: Dict[str, Dict[str, List[str]]] = Field(
94
- ..., # Required field, no default
95
- description="IAM domain permissions granted by this subscription (domain -> IAM unit type -> list of unit references)"
92
+ # IAM permissions structure - simplified flattened list
93
+ granted_iam_permissions: List[UserPermission] = Field(
94
+ default_factory=list,
95
+ description="IAM permissions granted by this subscription"
96
96
  )
97
97
 
98
98
  fallback_plan_id: Optional[str] = Field(
99
- ..., # Required field (can be None), no default
99
+ default=None, # Optional field with None default
100
100
  description="ID of the plan to fall back to if this subscription expires"
101
101
  )
102
102
 
@@ -142,6 +142,46 @@ class Subscription(BaseDataModel):
142
142
  description="Additional metadata for the subscription"
143
143
  )
144
144
 
145
+ @model_validator(mode='before')
146
+ @classmethod
147
+ def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
148
+ """
149
+ Ensures the id field exists by generating it using UUID if needed.
150
+ """
151
+ if not isinstance(data, dict):
152
+ return data
153
+
154
+ # If id is already provided and non-empty, leave it alone
155
+ if data.get('id'):
156
+ return data
157
+
158
+ # Generate a UUID-based id if not provided
159
+ data['id'] = str(uuid.uuid4())
160
+ return data
161
+
162
+ @model_validator(mode='before')
163
+ @classmethod
164
+ def auto_calculate_cycle_end_date(cls, data: Dict[str, Any]) -> Dict[str, Any]:
165
+ """
166
+ Auto-calculate cycle_end_date if not provided, based on cycle_start_date,
167
+ validity_time_length, and validity_time_unit.
168
+ """
169
+ if not isinstance(data, dict):
170
+ return data
171
+
172
+ # Only calculate if cycle_end_date is not already provided
173
+ if 'cycle_end_date' not in data or data['cycle_end_date'] is None:
174
+ cycle_start_date = data.get('cycle_start_date')
175
+ validity_time_length = data.get('validity_time_length')
176
+ validity_time_unit = data.get('validity_time_unit')
177
+
178
+ if cycle_start_date and validity_time_length and validity_time_unit:
179
+ data['cycle_end_date'] = cls.calculate_cycle_end_date(
180
+ cycle_start_date, validity_time_length, validity_time_unit
181
+ )
182
+
183
+ return data
184
+
145
185
  # Helper method to calculate cycle end date
146
186
  @classmethod
147
187
  def calculate_cycle_end_date(cls, start_date: datetime, validity_length: int, validity_unit: str) -> datetime:
@@ -162,6 +202,8 @@ class Subscription(BaseDataModel):
162
202
  # Methods for subscription management
163
203
  def is_active(self) -> bool:
164
204
  """Check if the subscription is currently active."""
205
+ if not self.cycle_end_date:
206
+ return False
165
207
  now = datetime.now(timezone.utc)
166
208
  return (
167
209
  self.status == SubscriptionStatus.ACTIVE and
@@ -170,11 +212,15 @@ class Subscription(BaseDataModel):
170
212
 
171
213
  def is_expired(self) -> bool:
172
214
  """Check if the subscription has expired."""
215
+ if not self.cycle_end_date:
216
+ return True
173
217
  now = datetime.now(timezone.utc)
174
218
  return now > self.cycle_end_date
175
219
 
176
220
  def days_remaining(self) -> int:
177
221
  """Calculate the number of days remaining in the subscription."""
222
+ if not self.cycle_end_date:
223
+ return 0
178
224
  now = datetime.now(timezone.utc)
179
225
  if now > self.cycle_end_date:
180
226
  return 0
@@ -1,6 +1,6 @@
1
1
  from typing import Optional, Dict, Any, List
2
2
  from datetime import datetime
3
- from pydantic import BaseModel, Field, EmailStr, ConfigDict, field_validator
3
+ from pydantic import BaseModel, Field, EmailStr, ConfigDict, field_validator, model_validator
4
4
 
5
5
  class UserAuth(BaseModel):
6
6
  """Comprehensive authentication model for user credentials and auth operations"""
@@ -8,17 +8,18 @@ class UserAuth(BaseModel):
8
8
 
9
9
  # Core authentication fields
10
10
  email: EmailStr = Field(..., description="User's email address")
11
+ display_name: Optional[str] = Field(None, description="User's display name")
11
12
  password: Optional[str] = Field(None, min_length=6, description="User's password (for creation/update only)")
12
13
 
13
14
  # Firebase Auth specific fields
14
- firebase_uid: Optional[str] = Field(None, description="Firebase Auth UID")
15
+ firebase_uid: Optional[str] = Field(default=None, description="Firebase Auth UID")
15
16
  provider_id: str = Field(default="password", description="Authentication provider ID")
16
17
  email_verified: bool = Field(default=False, description="Whether email is verified")
17
18
  disabled: bool = Field(default=False, description="Whether user account is disabled")
18
19
 
19
20
  # Multi-factor authentication
20
21
  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
+ phone_number: Optional[str] = Field(default=None, description="Phone number for SMS MFA")
22
23
 
23
24
  # Custom claims and metadata
24
25
  custom_claims: Dict[str, Any] = Field(default_factory=dict, description="Firebase custom claims")
@@ -28,14 +29,14 @@ class UserAuth(BaseModel):
28
29
  provider_data: List[Dict[str, Any]] = Field(default_factory=list, description="Provider-specific data")
29
30
 
30
31
  # 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")
32
+ created_at: Optional[datetime] = Field(default=None, description="Account creation timestamp")
33
+ last_sign_in: Optional[datetime] = Field(default=None, description="Last sign-in timestamp")
34
+ last_refresh: Optional[datetime] = Field(default=None, description="Last token refresh timestamp")
34
35
 
35
36
  # 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")
37
+ password_hash: Optional[str] = Field(default=None, description="Password hash (internal use only)")
38
+ password_salt: Optional[str] = Field(default=None, description="Password salt (internal use only)")
39
+ valid_since: Optional[datetime] = Field(default=None, description="Timestamp since when tokens are valid")
39
40
 
40
41
  @field_validator('phone_number')
41
42
  @classmethod
@@ -61,4 +62,12 @@ class UserAuth(BaseModel):
61
62
  if claim in reserved_claims:
62
63
  raise ValueError(f'Custom claim "{claim}" is reserved by Firebase')
63
64
 
64
- return v
65
+ return v
66
+
67
+ @model_validator(mode='before')
68
+ @classmethod
69
+ def check_password_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
70
+ """Ensure that password and password_hash are not set simultaneously."""
71
+ if values.get('password') and values.get('password_hash'):
72
+ raise ValueError('Cannot set both password and password_hash')
73
+ return values
@@ -1,11 +1,10 @@
1
1
  """ User Profile model for storing personal information and settings. """
2
- from datetime import date, datetime, timezone
3
- from typing import Set, Optional, ClassVar, Dict, Any, List
4
- from pydantic import EmailStr, Field, ConfigDict, field_validator, model_validator, computed_field
5
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject
6
- from .base_data_model import BaseDataModel
2
+ from datetime import date, datetime
7
3
  import re # Add re import
8
-
4
+ from typing import Set, Optional, ClassVar, Dict, Any, List
5
+ from pydantic import EmailStr, Field, ConfigDict, model_validator, field_validator
6
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, IAMUserType
7
+ from ..base_data_model import BaseDataModel
9
8
  # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
10
9
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
11
10
 
@@ -18,7 +17,7 @@ class UserProfile(BaseDataModel):
18
17
 
19
18
  # Class constants
20
19
  VERSION: ClassVar[float] = 5.0 # Incremented version for primary_usertype addition
21
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
20
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.USER))
22
21
  OBJ_REF: ClassVar[str] = "userprofile"
23
22
 
24
23
  schema_version: float = Field(
@@ -27,26 +26,28 @@ class UserProfile(BaseDataModel):
27
26
  description="Version of this Class == version of DB Schema"
28
27
  )
29
28
 
30
- id: str = Field(
31
- default="", # Will be auto-generated from user_uid if not provided
29
+ id: Optional[str] = Field(
30
+ default=None, # Will be auto-generated from user_uid if not provided
32
31
  description=f"User Profile ID, format: {OBJ_REF}_user_uid"
33
32
  )
34
33
 
35
34
  user_uid: str = Field(
36
35
  ...,
37
- description="User UID from Firebase Auth"
36
+ min_length=1,
37
+ description="User UID from Firebase Auth",
38
+ frozen=True
38
39
  )
39
40
 
40
41
  # Added primary_usertype field for main role categorization
41
- primary_usertype: str = Field(
42
+ primary_usertype: IAMUserType = Field(
42
43
  ...,
43
- description="Primary user type (e.g., customer, internal, admin, superadmin)"
44
+ description="Primary user type from IAMUserType enum"
44
45
  )
45
46
 
46
47
  # Renamed usertypes to secondary_usertypes
47
- secondary_usertypes: List[str] = Field(
48
+ secondary_usertypes: List[IAMUserType] = Field(
48
49
  default_factory=list,
49
- description="List of secondary user types"
50
+ description="List of secondary user types from IAMUserType enum"
50
51
  )
51
52
 
52
53
  # Rest of the fields remain the same
@@ -105,24 +106,38 @@ class UserProfile(BaseDataModel):
105
106
 
106
107
  # Remove audit fields as they're inherited from BaseDataModel
107
108
 
109
+ @field_validator('user_uid')
110
+ @classmethod
111
+ def validate_user_uid(cls, v: str) -> str:
112
+ """Validate that user_uid is not empty string."""
113
+ if not v or not v.strip():
114
+ raise ValueError("user_uid cannot be empty or whitespace-only")
115
+ return v.strip()
116
+
108
117
  @model_validator(mode='before')
109
118
  @classmethod
110
119
  def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
111
120
  """
112
- Ensures the id field exists by generating it from user_uid if needed.
121
+ Ensures the id field exists and matches expected format, or generates it from user_uid.
113
122
  This runs BEFORE validation, guaranteeing id will be present for validators.
114
123
  """
115
124
  if not isinstance(data, dict):
116
125
  return data
117
126
 
118
- # If id is already in the data, leave it alone
119
- if 'id' in data and data['id']:
120
- return data
127
+ user_uid = data.get('user_uid')
128
+ if not user_uid:
129
+ return data # Let field validation handle missing user_uid
121
130
 
122
- # If user_uid exists but id doesn't, generate id from user_uid
123
- if 'user_uid' in data and data['user_uid']:
124
- data['id'] = f"{cls.OBJ_REF}_{data['user_uid']}"
131
+ expected_id = f"{cls.OBJ_REF}_{user_uid}"
125
132
 
133
+ # If id is already provided, validate it matches expected format
134
+ if data.get('id'):
135
+ if data['id'] != expected_id:
136
+ raise ValueError(f"Invalid id format. Expected '{expected_id}', got '{data['id']}'")
137
+ return data
138
+
139
+ # If id is not provided, generate it from user_uid
140
+ data['id'] = expected_id
126
141
  return data
127
142
 
128
143
  @model_validator(mode='before')
@@ -156,4 +171,21 @@ class UserProfile(BaseDataModel):
156
171
  # Fallback if no email or username provided
157
172
  data['username'] = "user"
158
173
 
174
+ return data
175
+
176
+ @model_validator(mode='before')
177
+ @classmethod
178
+ def convert_datetime_to_date(cls, data: Any) -> Any:
179
+ """
180
+ Convert datetime objects to date objects for date fields.
181
+ This handles the case where Firestore returns datetime objects
182
+ but the model expects date objects (e.g., dob field).
183
+ """
184
+ if not isinstance(data, dict):
185
+ return data
186
+
187
+ # Handle dob field specifically
188
+ if 'dob' in data and isinstance(data['dob'], datetime):
189
+ data['dob'] = data['dob'].date()
190
+
159
191
  return data