ipulse-shared-core-ftredge 7.1.1__py3-none-any.whl → 8.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.

@@ -1,6 +1,6 @@
1
1
  # pylint: disable=missing-module-docstring
2
2
  from .models import ( UserAuth, UserProfile,Subscription,
3
- UserStatus, UserProfileUpdate,
3
+ UserStatus, IAMUnitRefAssignment, UserProfileUpdate,
4
4
  OrganizationProfile, BaseAPIResponse,
5
5
  CustomJSONResponse )
6
6
 
@@ -160,7 +160,7 @@ async def authorizeAPIRequest(
160
160
  ) -> Dict[str, Any]:
161
161
  """
162
162
  Authorize API request based on user status and OPA policies.
163
- Note: This expects an actual Firestore client instance, not a dependency.
163
+ Enhanced with credit check information.
164
164
  """
165
165
  try:
166
166
  # Extract fields for both PATCH and POST if not provided
@@ -196,6 +196,7 @@ async def authorizeAPIRequest(
196
196
  "request_resource_fields": request_resource_fields
197
197
  }
198
198
 
199
+ ####!!!!!!!!!! OPA call
199
200
  # Query OPA
200
201
  opa_url = f"{os.getenv('OPA_SERVER_URL', 'http://localhost:8181')}{os.getenv('OPA_DECISION_PATH', '/v1/data/http/authz/ingress/decision')}"
201
202
  logger.debug(f"Attempting to connect to OPA at: {opa_url}")
@@ -237,11 +238,17 @@ async def authorizeAPIRequest(
237
238
  }
238
239
  )
239
240
 
241
+ # Extract credit check information from the OPA response
242
+ credit_check = {}
243
+ if "credit_check" in result.get("result", {}):
244
+ credit_check = result["result"]["credit_check"]
245
+
240
246
  # More descriptive metadata about the data freshness
241
247
  return {
242
248
  "used_cached_status": cache_used,
243
249
  "required_fresh_status": force_fresh,
244
- "status_retrieved_at": datetime.now(timezone.utc).isoformat()
250
+ "status_retrieved_at": datetime.now(timezone.utc).isoformat(),
251
+ "credit_check": credit_check
245
252
  }
246
253
 
247
254
  except (AuthorizationError, ResourceNotFoundError):
@@ -268,6 +275,7 @@ def _should_force_fresh_status(request: Request) -> bool:
268
275
  credit_sensitive_patterns = [
269
276
  'prediction',
270
277
  'user-statuses',
278
+ 'historic'
271
279
  ]
272
280
  # Methods that require fresh status
273
281
  sensitive_methods = {'post', 'patch', 'put', 'delete'}
@@ -10,8 +10,8 @@ logger = logging.getLogger(__name__)
10
10
  @lru_cache()
11
11
  def get_db() -> firestore.Client:
12
12
  """
13
- Dependency function to inject the Firestore client.
14
13
  !!! THIS IS JUST AN EXAMPLE !!!
14
+ Dependency function to inject the Firestore client.
15
15
  !!! Each service implementing this should override this function with their own Firebase initialization. !!!
16
16
  """
17
17
  logger.info("Base get_db dependency called - this should be overridden by the implementing service")
@@ -1,6 +1,6 @@
1
1
  from .user_profile import UserProfile
2
2
  from .subscription import Subscription
3
- from .user_status import UserStatus
3
+ from .user_status import UserStatus, IAMUnitRefAssignment
4
4
  from .user_profile_update import UserProfileUpdate
5
5
  from .user_auth import UserAuth
6
6
  from .organization_profile import OrganizationProfile
@@ -1,4 +1,4 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
  from typing import ClassVar
3
3
  from pydantic import BaseModel, Field, ConfigDict, field_validator
4
4
  import dateutil.parser
@@ -20,17 +20,17 @@ class BaseDataModel(BaseModel):
20
20
  )
21
21
 
22
22
  # Audit fields
23
- creat_date: datetime = Field(default_factory=datetime.utcnow, frozen=True)
24
- creat_by_user: str = Field(..., frozen=True)
25
- updt_date: datetime = Field(default_factory=datetime.utcnow)
26
- updt_by_user: str = Field(...)
23
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
24
+ created_by: str = Field(..., frozen=True)
25
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
26
+ updated_by: str = Field(...)
27
27
 
28
28
  @classmethod
29
29
  def get_collection_name(cls) -> str:
30
30
  """Generate standard collection name"""
31
31
  return f"{cls.DOMAIN}_{cls.OBJ_REF}s"
32
32
 
33
- @field_validator('creat_date', 'updt_date', mode='before')
33
+ @field_validator('created_at', 'updated_at', mode='before')
34
34
  @classmethod
35
35
  def parse_datetime(cls, v: any) -> datetime:
36
36
  if isinstance(v, datetime):
@@ -26,8 +26,8 @@ class OrganizationProfile(BaseDataModel):
26
26
  Organisation model representing business entities in the system.
27
27
  Supports both retail and non-retail customer types with different validation rules.
28
28
  """
29
- model_config = ConfigDict(frozen=True, extra="forbid")
30
-
29
+ model_config = ConfigDict(frozen=False, extra="forbid") # Changed frozen to False to allow id assignment
30
+
31
31
  # Class constants
32
32
  VERSION: ClassVar[float] = 4.1
33
33
  DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.ORGANIZATION.name))
@@ -38,7 +38,7 @@ class OrganizationProfile(BaseDataModel):
38
38
  description="Version of this Class == version of DB Schema",
39
39
  frozen=True
40
40
  )
41
-
41
+
42
42
  org_uid: str = Field(
43
43
  default_factory=lambda: uuid.uuid4().hex,
44
44
  description="Unique identifier for the organisation",
@@ -46,7 +46,7 @@ class OrganizationProfile(BaseDataModel):
46
46
  )
47
47
 
48
48
  id: str = Field(
49
- default=None,
49
+ ..., # Make it required
50
50
  description="Organisation ID, format: {OBJ_REF}_{org_uid}"
51
51
  )
52
52
 
@@ -1,28 +1,30 @@
1
1
  from datetime import datetime, timezone
2
2
  from dateutil.relativedelta import relativedelta
3
- from typing import Set, Optional, ClassVar
4
- from pydantic import Field, ConfigDict
5
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject, SubscriptionPlan
3
+ import uuid
4
+ from typing import Set, Optional, ClassVar, Dict, Any, List, Union
5
+ from pydantic import Field, ConfigDict, computed_field
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
6
8
  from .base_data_model import BaseDataModel
7
- # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
9
+ # ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
8
10
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
9
11
 
10
12
 
11
13
  DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlan.FREE
12
- DEFAULT_SUBSCRIPTION_STATUS = "active"
14
+ DEFAULT_SUBSCRIPTION_STATUS = SubscriptionStatus.ACTIVE
13
15
 
14
16
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
15
17
  class Subscription(BaseDataModel):
16
18
  """
17
- Represents a single subscription cycle.
19
+ Represents a single subscription cycle with enhanced flexibility and tracking.
18
20
  """
19
21
 
20
22
  model_config = ConfigDict(frozen=True, extra="forbid")
21
23
 
22
- VERSION: ClassVar[float] = 1.1
23
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.SUBSCRIPTION_PLAN.name))
24
+ VERSION: ClassVar[float] = 2.9 # Incremented version for new fields
25
+ DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.SUBSCRIPTION.name))
24
26
  OBJ_REF: ClassVar[str] = "subscription"
25
-
27
+
26
28
  # System-managed fields (read-only)
27
29
  schema_version: float = Field(
28
30
  default=VERSION,
@@ -30,31 +32,148 @@ class Subscription(BaseDataModel):
30
32
  frozen=True
31
33
  )
32
34
 
35
+ # Unique identifier for this specific subscription instance - now auto-generated
36
+ uuid: str = Field(
37
+ default_factory=lambda: str(uuid.uuid4()),
38
+ description="Unique identifier for this subscription instance"
39
+ )
40
+
41
+ # Plan identification
33
42
  plan_name: SubscriptionPlan = Field(
34
- default=DEFAULT_SUBSCRIPTION_PLAN,
43
+ ..., # Required field, no default
35
44
  description="Subscription Plan Name"
36
45
  )
37
46
 
38
- plan_version: float = Field(
39
- default=1.0,
47
+ plan_version: int = Field(
48
+ ..., # Required field, no default
40
49
  description="Version of the subscription plan"
41
50
  )
42
51
 
52
+ @computed_field
53
+ def plan_id(self) -> str:
54
+ """
55
+ Generate a plan identifier combining plan name and version.
56
+ Format: {plan_name}_{plan_version}
57
+ Example: "free_subscription_1"
58
+ """
59
+ return f"{self.plan_name.value}_{self.plan_version}"
60
+
61
+ # Cycle duration fields
43
62
  cycle_start_date: datetime = Field(
44
- default=datetime.now(timezone.utc),
63
+ ..., # Required field, no default
45
64
  description="Subscription Cycle Start Date"
46
65
  )
47
- cycle_end_date: datetime = Field(
48
- default=lambda: datetime.now(timezone.utc) + relativedelta(years=1),
49
- description="Subscription Cycle End Date"
66
+
67
+ # New fields for more flexible cycle management
68
+ validity_time_length: int = Field(
69
+ ..., # Required field, no default
70
+ description="Length of subscription validity period (e.g., 1, 3, 12)"
71
+ )
72
+
73
+ validity_time_unit: str = Field(
74
+ ..., # Required field, no default
75
+ description="Unit of subscription validity ('minute', 'hour', 'day', 'week', 'month', 'year')"
50
76
  )
77
+
78
+ # Computed cycle_end_date based on start date and validity
79
+ @computed_field
80
+ def cycle_end_date(self) -> datetime:
81
+ """Calculate the end date based on start date and validity period."""
82
+ if self.validity_time_unit == "minute":
83
+ return self.cycle_start_date + relativedelta(minutes=self.validity_time_length)
84
+ elif self.validity_time_unit == "hour":
85
+ return self.cycle_start_date + relativedelta(hours=self.validity_time_length)
86
+ elif self.validity_time_unit == "day":
87
+ return self.cycle_start_date + relativedelta(days=self.validity_time_length)
88
+ elif self.validity_time_unit == "week":
89
+ return self.cycle_start_date + relativedelta(weeks=self.validity_time_length)
90
+ elif self.validity_time_unit == "year":
91
+ return self.cycle_start_date + relativedelta(years=self.validity_time_length)
92
+ else: # Default to months
93
+ return self.cycle_start_date + relativedelta(months=self.validity_time_length)
94
+
95
+ # Renewal and status fields
51
96
  auto_renew: bool = Field(
52
- default=True,
97
+ ..., # Required field, no default
53
98
  description="Auto-renewal status"
54
99
  )
55
- status: str = Field(
56
- default=DEFAULT_SUBSCRIPTION_STATUS,
57
- description="Subscription Status (active, trial, inactive, etc.)"
100
+
101
+ status: SubscriptionStatus = Field(
102
+ ..., # Required field, no default
103
+ description="Subscription Status (active, trial, pending_confirmation, etc.)"
104
+ )
105
+
106
+ # New fields for enhanced subscription management
107
+ # Update the type definition to use string keys for IAMUnitType
108
+ iam_domain_permissions: Dict[str, Dict[str, List[str]]] = Field(
109
+ ..., # Required field, no default
110
+ description="IAM domain permissions granted by this subscription (domain -> IAM unit type -> list of unit references)"
111
+ )
112
+
113
+ fallback_plan_id: Optional[str] = Field(
114
+ ..., # Required field (can be None), no default
115
+ description="ID of the plan to fall back to if this subscription expires"
116
+ )
117
+
118
+ price_paid_usd: float = Field(
119
+ ..., # Required field, no default
120
+ description="Amount paid for this subscription in USD"
121
+ )
122
+
123
+ payment_ref: Optional[str] = Field(
124
+ default=None,
125
+ description="Reference to payment transaction"
126
+ )
127
+
128
+ # New fields moved from metadata to direct attributes
129
+ subscription_based_insight_credits_per_update: int = Field(
130
+ default=0,
131
+ description="Number of insight credits to add on each update"
132
+ )
133
+
134
+ subscription_based_insight_credits_update_freq_h: int = Field(
135
+ default=24,
136
+ description="Frequency of insight credits update in hours"
137
+ )
138
+
139
+ extra_insight_credits_per_cycle: int = Field(
140
+ default=0,
141
+ description="Additional insight credits granted per subscription cycle"
142
+ )
143
+
144
+ voting_credits_per_update: int = Field(
145
+ default=0,
146
+ description="Number of voting credits to add on each update"
147
+ )
148
+
149
+ voting_credits_update_freq_h: int = Field(
150
+ default=62,
151
+ description="Frequency of voting credits update in hours"
152
+ )
153
+
154
+ # General metadata for extensibility
155
+ metadata: Dict[str, Any] = Field(
156
+ default_factory=dict,
157
+ description="Additional metadata for the subscription"
58
158
  )
59
159
 
60
- # Remove audit fields as they're inherited from BaseDataModel
160
+ # Methods for subscription management
161
+ def is_active(self) -> bool:
162
+ """Check if the subscription is currently active."""
163
+ now = datetime.now(timezone.utc)
164
+ return (
165
+ self.status == SubscriptionStatus.ACTIVE and
166
+ self.cycle_start_date <= now <= self.cycle_end_date
167
+ )
168
+
169
+ def is_expired(self) -> bool:
170
+ """Check if the subscription has expired."""
171
+ now = datetime.now(timezone.utc)
172
+ return now > self.cycle_end_date
173
+
174
+ def days_remaining(self) -> int:
175
+ """Calculate the number of days remaining in the subscription."""
176
+ now = datetime.now(timezone.utc)
177
+ if now > self.cycle_end_date:
178
+ return 0
179
+ return (self.cycle_end_date - now).days
@@ -1,96 +1,125 @@
1
- """ User Profile model representing user information. """
2
- from datetime import date
3
- from typing import Set, Optional, ClassVar
4
- from pydantic import EmailStr, Field, ConfigDict, field_validator
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
5
  from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject
6
6
  from .base_data_model import BaseDataModel
7
7
 
8
- # # Revision history (as model metadata)
9
- # CLASS_ORIGIN_AUTHOR: ClassVar[str] = "Russlan Ramdowar;russlan@ftredge.com"
10
- # CLASS_ORGIN_DATE: ClassVar[datetime] = datetime(2024, 1, 16, 20, 5)
8
+ # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
9
+ # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
10
+
11
+ ############################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! #################################
11
12
  class UserProfile(BaseDataModel):
12
13
  """
13
- User Profile model representing user information and metadata.
14
- Contains both system-managed and user-editable fields.
14
+ User Profile model for storing personal information and settings.
15
15
  """
16
- model_config = ConfigDict(frozen=True, extra="forbid")
16
+ model_config = ConfigDict(frozen=False, extra="forbid") # Allow field modification
17
17
 
18
- # Metadata as class variables
19
- VERSION: ClassVar[float] = 4.1
18
+ # Class constants
19
+ VERSION: ClassVar[float] = 5.0 # Incremented version for primary_user_type addition
20
20
  DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
21
21
  OBJ_REF: ClassVar[str] = "userprofile"
22
-
23
- # System-managed fields (read-only)
22
+
24
23
  schema_version: float = Field(
25
24
  default=VERSION,
26
- description="Version of this Class == version of DB Schema",
27
- frozen=True
25
+ frozen=True,
26
+ description="Version of this Class == version of DB Schema"
28
27
  )
29
-
30
- id : str = Field(
31
- ...,
32
- description="User ID, propagated from Firebase Auth"
28
+
29
+ id: str = Field(
30
+ ..., # Required, but will be auto-generated if not provided
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, propagated from Firebase Auth"
36
+ description="User UID from Firebase Auth"
38
37
  )
39
38
 
39
+ # Added primary_user_type field for main role categorization
40
+ primary_user_type: str = Field(
41
+ ...,
42
+ description="Primary user type (e.g., customer, internal, admin, superadmin)"
43
+ )
40
44
 
45
+ # Renamed user_types to secondary_user_types
46
+ secondary_user_types: List[str] = Field(
47
+ default_factory=list,
48
+ description="List of secondary user types"
49
+ )
50
+
51
+ # Rest of the fields remain the same
41
52
  email: EmailStr = Field(
42
53
  ...,
43
- description="Propagated from Firebase Auth",
54
+ description="Email address",
44
55
  frozen=True
45
56
  )
46
57
  organizations_uids: Set[str] = Field(
47
58
  default_factory=set,
48
- description="Depends on Subscription Plan, Regularly Updated"
59
+ description="Organization UIDs the user belongs to"
49
60
  )
50
-
61
+
51
62
  # System identification (read-only)
52
- provider_id: str = Field(frozen=True)
53
- aliases: Optional[Set[str]] = Field(
54
- default=None
63
+ provider_id: str = Field(
64
+ ...,
65
+ description="User provider ID",
66
+ frozen=True
55
67
  )
56
-
68
+ aliases: Optional[Dict[str, str]] = Field(
69
+ default=None,
70
+ description="User aliases. With alias as key and description as value."
71
+ )
72
+
57
73
  # User-editable fields
58
74
  username: Optional[str] = Field(
59
75
  default=None,
60
76
  max_length=50,
61
- pattern="^[a-zA-Z0-9_-]+$"
77
+ pattern="^[a-zA-Z0-9_-]+$",
78
+ description="Username (public display name)"
62
79
  )
63
80
  dob: Optional[date] = Field(
64
81
  default=None,
65
- description="Date of Birth"
82
+ description="Date of birth"
66
83
  )
67
84
  first_name: Optional[str] = Field(
68
85
  default=None,
69
- max_length=100
86
+ max_length=100,
87
+ description="First name"
70
88
  )
71
89
  last_name: Optional[str] = Field(
72
90
  default=None,
73
- max_length=100
91
+ max_length=100,
92
+ description="Last name"
74
93
  )
75
94
  mobile: Optional[str] = Field(
76
95
  default=None,
77
96
  pattern=r"^\+?[1-9]\d{1,14}$", # Added 'r' prefix for raw string
78
- description="E.164 format phone number"
97
+ description="Mobile phone number"
98
+ )
99
+
100
+ metadata: Dict[str, Any] = Field(
101
+ default_factory=dict,
102
+ description="Additional metadata for the user"
79
103
  )
80
104
 
81
105
  # Remove audit fields as they're inherited from BaseDataModel
82
106
 
83
- @field_validator('id', mode='before')
107
+ @model_validator(mode='before')
84
108
  @classmethod
85
- def validate_or_generate_id(cls, v: Optional[str], info) -> str:
86
- """Validate or generate user ID based on user_uid."""
87
- # If id is already provided (Firebase Auth case), return it
88
- if v:
89
- return v
90
-
91
- # Fallback: generate from user_uid if needed
92
- values = info.data
93
- user_uid = values.get('user_uid')
94
- if not user_uid:
95
- raise ValueError("Either id or user_uid must be provided")
96
- return f"{cls.OBJ_REF}_{user_uid}"
109
+ def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
110
+ """
111
+ Ensures the id field exists by generating it from user_uid if needed.
112
+ This runs BEFORE validation, guaranteeing id will be present for validators.
113
+ """
114
+ if not isinstance(data, dict):
115
+ return data
116
+
117
+ # If id is already in the data, leave it alone
118
+ if 'id' in data and data['id']:
119
+ return data
120
+
121
+ # If user_uid exists but id doesn't, generate id from user_uid
122
+ if 'user_uid' in data and data['user_uid']:
123
+ data['id'] = f"{cls.OBJ_REF}_{data['user_uid']}"
124
+
125
+ return data