ipulse-shared-core-ftredge 19.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 +2 -2
  16. ipulse_shared_core_ftredge/monitoring/tracemon.py +320 -0
  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-19.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 -483
  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-19.0.1.dist-info/RECORD +0 -41
  45. {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/WHEEL +0 -0
  46. {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/licenses/LICENCE +0 -0
  47. {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,430 @@
1
+ """ User Status model for tracking user subscription and access rights. """
2
+ from datetime import datetime, timezone
3
+ from typing import Set, Optional, Dict, List, ClassVar, Any
4
+ from pydantic import Field, ConfigDict, model_validator, field_validator
5
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject
6
+ from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnit
7
+ from .user_subscription import UserSubscription
8
+ from ..base_data_model import BaseDataModel
9
+ from .user_permissions import UserPermission
10
+
11
+
12
+
13
+ ############################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! #################################
14
+ class UserStatus(BaseDataModel):
15
+ """
16
+ User Status model for tracking user subscription and access rights.
17
+ """
18
+ # Set frozen=False to allow modification of attributes
19
+ model_config = ConfigDict(frozen=False, extra="forbid")
20
+
21
+ # Class constants
22
+ VERSION: ClassVar[float] = 6.0 # Major version bump for flattened IAM permissions structure
23
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.USER))
24
+ OBJ_REF: ClassVar[str] = "userstatus"
25
+
26
+ # Centralized collection name and document ID prefix
27
+ COLLECTION_NAME: ClassVar[str] = "papp_core_user_userstatuss"
28
+
29
+
30
+ # System-managed fields
31
+ schema_version: float = Field(
32
+ default=VERSION,
33
+ frozen=True,
34
+ description="Version of this Class == version of DB Schema"
35
+ )
36
+
37
+ id: Optional[str] = Field(
38
+ default=None, # Will be auto-generated from user_uid if not provided
39
+ description=f"User ID, format: {OBJ_REF}_user_uid"
40
+ )
41
+
42
+ user_uid: str = Field(
43
+ ...,
44
+ min_length=1,
45
+ description="User UID from Firebase Auth",
46
+ frozen=True
47
+ )
48
+
49
+ # Added organizations field for consistency with UserProfile
50
+ organizations_uids: Set[str] = Field(
51
+ default_factory=set,
52
+ description="Organization UIDs the user belongs to"
53
+ )
54
+
55
+ # Simplified IAM permissions structure - flattened for easier management
56
+ iam_permissions: List[UserPermission] = Field(
57
+ default_factory=list,
58
+ description="List of all IAM permission assignments for this user"
59
+ )
60
+
61
+ # Changed from dictionary to single Optional subscription
62
+ active_subscription: Optional[UserSubscription] = Field(
63
+ default=None,
64
+ description="The user's currently active subscription, if any"
65
+ )
66
+
67
+ # Credit management fields
68
+ sbscrptn_based_insight_credits: Optional[int] = Field(
69
+ default=0,
70
+ ge=0, # Must be >= 0
71
+ description="Subscription-based insight credits (expire with subscription)"
72
+ )
73
+
74
+ sbscrptn_based_insight_credits_updtd_on: datetime = Field(
75
+ default_factory=lambda: datetime.now(timezone.utc),
76
+ description="Last update timestamp for subscription credits"
77
+ )
78
+
79
+ extra_insight_credits: Optional[int] = Field(
80
+ default=0,
81
+ ge=0, # Must be >= 0
82
+ description="Additional purchased insight credits (non-expiring)"
83
+ )
84
+
85
+ extra_insight_credits_updtd_on: datetime = Field(
86
+ default_factory=lambda: datetime.now(timezone.utc),
87
+ description="Last update timestamp for extra credits"
88
+ )
89
+
90
+ voting_credits: Optional[int] = Field(
91
+ default=0,
92
+ ge=0, # Must be >= 0
93
+ description="Voting credits for user"
94
+ )
95
+
96
+ voting_credits_updtd_on: datetime = Field(
97
+ default_factory=lambda: datetime.now(timezone.utc),
98
+ description="Last update timestamp for voting credits"
99
+ )
100
+
101
+ metadata: Dict[str, Any] = Field(
102
+ default_factory=dict,
103
+ description="Additional metadata for the user status"
104
+ )
105
+
106
+ @field_validator('user_uid')
107
+ @classmethod
108
+ def validate_user_uid(cls, v: str) -> str:
109
+ """Validate that user_uid is not empty string."""
110
+ if not v or not v.strip():
111
+ raise ValueError("user_uid cannot be empty or whitespace-only")
112
+ return v.strip()
113
+
114
+ @model_validator(mode='before')
115
+ @classmethod
116
+ def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
117
+ """
118
+ Ensures the id field exists and matches expected format, or generates it from user_uid.
119
+ This runs BEFORE validation, guaranteeing id will be present for validators.
120
+ """
121
+ if not isinstance(data, dict):
122
+ return data
123
+
124
+ user_uid = data.get('user_uid')
125
+ if not user_uid:
126
+ return data # Let field validation handle missing user_uid
127
+
128
+ expected_id = f"{cls.OBJ_REF}_{user_uid}"
129
+
130
+ # If id is already provided, validate it matches expected format
131
+ if data.get('id'):
132
+ if data['id'] != expected_id:
133
+ raise ValueError(f"Invalid id format. Expected '{expected_id}', got '{data['id']}'")
134
+ return data
135
+
136
+ # If id is not provided, generate it from user_uid
137
+ data['id'] = expected_id
138
+ return data
139
+
140
+
141
+ ########################################################################
142
+ ############ ######### IAM Permission Management ######### #############
143
+ ########################################################################
144
+
145
+ def get_valid_permissions(
146
+ self,
147
+ domain: Optional[str] = None,
148
+ iam_unit_type: Optional[IAMUnit] = None,
149
+ permission_ref: Optional[str] = None
150
+ ) -> List[UserPermission]:
151
+ """
152
+ Get all valid (non-expired) permissions, optionally filtered.
153
+
154
+ Args:
155
+ domain: Filter by domain (e.g., 'papp')
156
+ iam_unit_type: Filter by IAM unit type (GROUP, ROLE, etc.)
157
+ permission_ref: Filter by permission reference name
158
+
159
+ Returns:
160
+ List of valid permissions matching the filters
161
+ """
162
+ valid_permissions = [perm for perm in self.iam_permissions if perm.is_valid()]
163
+
164
+ if domain is not None:
165
+ valid_permissions = [perm for perm in valid_permissions if perm.domain == domain]
166
+
167
+ if iam_unit_type is not None:
168
+ valid_permissions = [perm for perm in valid_permissions if perm.iam_unit_type == iam_unit_type]
169
+
170
+ if permission_ref is not None:
171
+ valid_permissions = [perm for perm in valid_permissions if perm.permission_ref == permission_ref]
172
+
173
+ return valid_permissions
174
+
175
+ def add_permission(self, permission: UserPermission) -> None:
176
+ """
177
+ Add a single permission assignment.
178
+
179
+ Args:
180
+ permission: UserPermission object to add
181
+ """
182
+ self.iam_permissions.append(permission)
183
+
184
+ def add_permission_from_fields(
185
+ self,
186
+ domain: str,
187
+ iam_unit_type: IAMUnit,
188
+ permission_ref: str,
189
+ source: str,
190
+ expires_at: Optional[datetime] = None,
191
+ granted_by: Optional[str] = None,
192
+ metadata: Optional[Dict[str, Any]] = None
193
+ ) -> None:
194
+ """
195
+ Add a permission assignment using individual fields (convenience method).
196
+
197
+ Args:
198
+ domain: The domain for the permission (e.g., 'papp')
199
+ iam_unit_type: Type of IAM assignment (GROUP, ROLE, etc.)
200
+ permission_ref: The name/identifier of the permission to add
201
+ source: Source identifier for this assignment (e.g., subscription ID)
202
+ expires_at: Optional expiration date
203
+ granted_by: Who granted this permission
204
+ metadata: Optional metadata for the assignment
205
+ """
206
+ permission = UserPermission(
207
+ domain=domain,
208
+ iam_unit_type=iam_unit_type,
209
+ permission_ref=permission_ref,
210
+ source=source,
211
+ expires_at=expires_at,
212
+ granted_by=granted_by,
213
+ metadata=metadata or {}
214
+ )
215
+ self.add_permission(permission)
216
+
217
+ def remove_permission(
218
+ self,
219
+ domain: Optional[str] = None,
220
+ iam_unit_type: Optional[IAMUnit] = None,
221
+ permission_ref: Optional[str] = None,
222
+ source: Optional[str] = None
223
+ ) -> int:
224
+ """
225
+ Remove permission assignments matching the criteria.
226
+ At least one filter criteria must be provided.
227
+
228
+ Args:
229
+ domain: Optional domain filter (e.g., 'papp')
230
+ iam_unit_type: Optional IAM assignment type filter (GROUP, ROLE, etc.)
231
+ permission_ref: Optional permission name/identifier filter
232
+ source: Optional source filter
233
+
234
+ Returns:
235
+ Number of permissions removed
236
+
237
+ Raises:
238
+ ValueError: If no filter criteria are provided
239
+ """
240
+ if not any([domain, iam_unit_type, permission_ref, source]):
241
+ raise ValueError("At least one filter criteria must be provided")
242
+
243
+ initial_count = len(self.iam_permissions)
244
+
245
+ def matches_criteria(perm: UserPermission) -> bool:
246
+ """Check if permission matches the removal criteria"""
247
+ if domain is not None and perm.domain != domain:
248
+ return False
249
+ if iam_unit_type is not None and perm.iam_unit_type != iam_unit_type:
250
+ return False
251
+ if permission_ref is not None and perm.permission_ref != permission_ref:
252
+ return False
253
+ if source is not None and perm.source != source:
254
+ return False
255
+ return True
256
+
257
+ self.iam_permissions = [perm for perm in self.iam_permissions if not matches_criteria(perm)]
258
+
259
+ return initial_count - len(self.iam_permissions)
260
+
261
+ def remove_all_permissions(self, source: Optional[str] = None) -> int:
262
+ """
263
+ Remove all permission assignments, optionally filtered by source.
264
+
265
+ Args:
266
+ source: Optional source filter (if None, removes all permissions)
267
+
268
+ Returns:
269
+ Number of permissions removed
270
+ """
271
+ initial_count = len(self.iam_permissions)
272
+
273
+ if source is None:
274
+ self.iam_permissions = []
275
+ else:
276
+ self.iam_permissions = [perm for perm in self.iam_permissions if perm.source != source]
277
+
278
+ return initial_count - len(self.iam_permissions)
279
+
280
+ def cleanup_expired_permissions(self, iam_unit_type: Optional[IAMUnit] = None) -> int:
281
+ """
282
+ Remove all expired permission assignments of a specific type or all types.
283
+
284
+ Args:
285
+ iam_unit_type: If provided, only remove this type of permissions
286
+
287
+ Returns:
288
+ Number of removed permission assignments
289
+ """
290
+ initial_count = len(self.iam_permissions)
291
+
292
+ if iam_unit_type is None:
293
+ self.iam_permissions = [perm for perm in self.iam_permissions if perm.is_valid()]
294
+ else:
295
+ self.iam_permissions = [
296
+ perm for perm in self.iam_permissions
297
+ if perm.is_valid() or perm.iam_unit_type != iam_unit_type
298
+ ]
299
+
300
+ return initial_count - len(self.iam_permissions)
301
+
302
+ ########################################################################
303
+ ############ ######### User Subscription Management ######### #############
304
+ ########################################################################
305
+
306
+ def update_user_permissions_from_subscription(self, subscription: UserSubscription, granted_by: Optional[str] = None) -> int:
307
+ """
308
+ Update user permissions based on a subscription.
309
+ Uses the new flattened permission structure.
310
+
311
+ Args:
312
+ subscription: Subscription to apply
313
+ granted_by: Who granted this permission (user ID, system process, etc.)
314
+
315
+ Returns:
316
+ Number of permission assignments added
317
+ """
318
+ added_count = 0
319
+ # Use the subscription plan_id as the source (which already contains "subscription" and version)
320
+ source = subscription.plan_id
321
+
322
+ # The granted_iam_permissions in Subscription is now List[UserPermission]
323
+ # We add each permission directly with subscription expiration
324
+
325
+ for permission in subscription.granted_iam_permissions:
326
+ # Create a new permission with subscription's expiration date and source
327
+ self.add_permission_from_fields(
328
+ domain=permission.domain,
329
+ iam_unit_type=permission.iam_unit_type,
330
+ permission_ref=permission.permission_ref,
331
+ source=source,
332
+ expires_at=subscription.cycle_end_date,
333
+ granted_by=granted_by
334
+ )
335
+ added_count += 1
336
+
337
+ return added_count
338
+
339
+ # Method instead of computed field
340
+ def is_subscription_active(self) -> bool:
341
+ """Check if the user has an active subscription."""
342
+ if self.active_subscription:
343
+ return self.active_subscription.is_active()
344
+ return False
345
+
346
+ # Method instead of computed field
347
+ def subscription_expires_in_days(self) -> Optional[int]:
348
+ """Get days until subscription expiration."""
349
+ if self.active_subscription and self.active_subscription.is_active():
350
+ return self.active_subscription.days_remaining()
351
+ return None
352
+
353
+ def apply_subscription(self, subscription: UserSubscription, add_associated_permissions: bool = True, remove_existing_subscription_permissions: bool = True, granted_by: Optional[str] = None) -> int:
354
+ """
355
+ Apply a subscription's benefits to the user status.
356
+ This updates credits, permissions, and sets the active subscription.
357
+
358
+ Args:
359
+ subscription: The subscription to apply
360
+ add_associated_permissions: If True, adds IAM permissions from the subscription
361
+ remove_existing_subscription_permissions: If True, removes IAM permissions from any existing subscription
362
+ granted_by: Who granted this permission (user ID, system process, etc.)
363
+
364
+ Returns:
365
+ Number of permissions added (0 if add_associated_permissions=False)
366
+ """
367
+ if not subscription:
368
+ return 0
369
+
370
+ permissions_added = 0
371
+
372
+ # Remove existing subscription permissions if requested
373
+ if remove_existing_subscription_permissions and self.active_subscription:
374
+ # Use the subscription plan_id as the source (which already contains "subscription" and version)
375
+ source = self.active_subscription.plan_id
376
+ removed_permissions = self.remove_all_permissions(source=source)
377
+ if removed_permissions > 0:
378
+ pass # Note: We don't return this count as it's not part of the "added" permissions
379
+
380
+ # Add IAM permissions from subscription if requested
381
+ if add_associated_permissions:
382
+ permissions_added = self.update_user_permissions_from_subscription(subscription, granted_by=granted_by)
383
+
384
+ # Update subscription-based credits
385
+ credits_per_update = subscription.subscription_based_insight_credits_per_update
386
+ if credits_per_update > 0:
387
+ self.sbscrptn_based_insight_credits = credits_per_update
388
+ self.sbscrptn_based_insight_credits_updtd_on = datetime.now(timezone.utc)
389
+
390
+ # Update voting credits directly from subscription attributes
391
+ voting_credits = subscription.voting_credits_per_update
392
+ if voting_credits > 0:
393
+ self.voting_credits = voting_credits
394
+ self.voting_credits_updtd_on = datetime.now(timezone.utc)
395
+
396
+ # Set as active subscription
397
+ self.active_subscription = subscription
398
+
399
+ return permissions_added
400
+
401
+ def revoke_subscription(self, remove_associated_permissions: bool = True) -> int:
402
+ """
403
+ Revoke the current subscription benefits.
404
+ This clears subscription-based credits and removes the active subscription.
405
+ Optionally also revokes associated IAM permissions.
406
+
407
+ Args:
408
+ remove_associated_permissions: If True, removes all IAM permissions
409
+ associated with the current subscription
410
+
411
+ Returns:
412
+ Number of permissions removed (0 if remove_associated_permissions=False or no subscription)
413
+ """
414
+ if not self.active_subscription:
415
+ return 0
416
+
417
+ permissions_removed = 0
418
+
419
+ # Revoke associated IAM permissions if requested
420
+ if remove_associated_permissions:
421
+ # Use the subscription plan_id as the source (which already contains "subscription" and version)
422
+ source = self.active_subscription.plan_id
423
+ permissions_removed = self.remove_all_permissions(source=source)
424
+
425
+ # Clear subscription-based credits and active subscription
426
+ self.sbscrptn_based_insight_credits = 0
427
+ self.sbscrptn_based_insight_credits_updtd_on = datetime.now(timezone.utc)
428
+ self.active_subscription = None
429
+
430
+ return permissions_removed
@@ -1,5 +1,5 @@
1
- from .microservmon import Microservmon
1
+ from .tracemon import TraceMon
2
2
 
3
3
  __all__ = [
4
- 'Microservmon',
4
+ 'TraceMon',
5
5
  ]