ipulse-shared-core-ftredge 7.2.1__tar.gz → 9.1.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 (38) hide show
  1. {ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-9.1.1}/PKG-INFO +4 -3
  2. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/setup.py +2 -2
  3. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
  4. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +10 -2
  5. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -1
  6. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +12 -4
  7. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +6 -6
  8. ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/models/subscription.py +190 -0
  9. ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/models/user_profile.py +125 -0
  10. ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/models/user_status.py +585 -0
  11. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +10 -10
  12. ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +46 -0
  13. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +4 -3
  14. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +1 -1
  15. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -1
  16. ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/resource_catalog_item.py +0 -115
  17. ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/subscription.py +0 -60
  18. ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -96
  19. ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/user_status.py +0 -106
  20. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/LICENCE +0 -0
  21. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/README.md +0 -0
  22. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/pyproject.toml +0 -0
  23. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/setup.cfg +0 -0
  24. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  25. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
  26. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  27. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +1 -1
  28. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -0
  29. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
  30. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
  31. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
  32. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py +0 -0
  33. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
  34. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
  35. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  36. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
  37. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  38. {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 7.2.1
3
+ Version: 9.1.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,10 +13,11 @@ 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>=5.7.1
16
+ Requires-Dist: ipulse_shared_base_ftredge>=6.4.1
17
17
  Dynamic: author
18
18
  Dynamic: classifier
19
19
  Dynamic: home-page
20
+ Dynamic: license-file
20
21
  Dynamic: requires-dist
21
22
  Dynamic: requires-python
22
23
  Dynamic: summary
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name='ipulse_shared_core_ftredge',
6
- version='7.2.1',
6
+ version='9.1.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>=5.7.1',
15
+ 'ipulse_shared_base_ftredge>=6.4.1',
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.',
@@ -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'}
@@ -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
@@ -3,10 +3,7 @@ import datetime as dt
3
3
  import json
4
4
  from pydantic import BaseModel, ConfigDict
5
5
  from fastapi.responses import JSONResponse
6
- from ipulse_shared_core_ftredge.utils import CustomJSONEncoder
7
-
8
-
9
-
6
+ from ipulse_shared_core_ftredge.utils import CustomJSONEncoder
10
7
 
11
8
 
12
9
  T = TypeVar('T')
@@ -30,6 +27,17 @@ class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
30
27
 
31
28
  class CustomJSONResponse(JSONResponse):
32
29
  def render(self, content) -> bytes:
30
+ # Handle Pydantic models to exclude computed fields
31
+ if isinstance(content, dict) and "data" in content and hasattr(content["data"], "model_dump"):
32
+ # If content["data"] is a Pydantic model, use model_dump with exclude_unset=True
33
+ # and exclude_computed=True to prevent serialization of computed fields
34
+ content = dict(content) # Create a copy to avoid modifying the original
35
+ content["data"] = content["data"].model_dump(
36
+ exclude_unset=True,
37
+ exclude_computed=True
38
+ )
39
+
40
+ # Use the CustomJSONEncoder for serialization
33
41
  return json.dumps(
34
42
  content,
35
43
  ensure_ascii=False,
@@ -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):
@@ -0,0 +1,190 @@
1
+ from datetime import datetime, timezone
2
+ from dateutil.relativedelta import relativedelta
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
9
+ # ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
10
+ # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
11
+
12
+
13
+ DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlan.FREE
14
+ DEFAULT_SUBSCRIPTION_STATUS = SubscriptionStatus.ACTIVE
15
+
16
+ ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
17
+ class Subscription(BaseDataModel):
18
+ """
19
+ Represents a single subscription cycle with enhanced flexibility and tracking.
20
+ """
21
+
22
+ model_config = ConfigDict(frozen=True, extra="forbid")
23
+
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))
26
+ OBJ_REF: ClassVar[str] = "subscription"
27
+
28
+ # System-managed fields (read-only)
29
+ schema_version: float = Field(
30
+ default=VERSION,
31
+ description="Version of this Class == version of DB Schema",
32
+ frozen=True
33
+ )
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
42
+ plan_name: SubscriptionPlan = Field(
43
+ ..., # Required field, no default
44
+ description="Subscription Plan Name"
45
+ )
46
+
47
+ plan_version: int = Field(
48
+ ..., # Required field, no default
49
+ description="Version of the subscription plan"
50
+ )
51
+
52
+ # Direct field instead of computed
53
+ plan_id: str = Field(
54
+ ..., # Required field, no default
55
+ description="Combined plan identifier (plan_name_plan_version)"
56
+ )
57
+
58
+ # Cycle duration fields
59
+ cycle_start_date: datetime = Field(
60
+ ..., # Required field, no default
61
+ description="Subscription Cycle Start Date"
62
+ )
63
+
64
+ # Direct field instead of computed
65
+ cycle_end_date: datetime = Field(
66
+ ..., # Required field, no default
67
+ description="Subscription Cycle End Date"
68
+ )
69
+
70
+ # Fields for cycle calculation
71
+ validity_time_length: int = Field(
72
+ ..., # Required field, no default
73
+ description="Length of subscription validity period (e.g., 1, 3, 12)"
74
+ )
75
+
76
+ validity_time_unit: str = Field(
77
+ ..., # Required field, no default
78
+ description="Unit of subscription validity ('minute', 'hour', 'day', 'week', 'month', 'year')"
79
+ )
80
+
81
+ # Renewal and status fields
82
+ auto_renew: bool = Field(
83
+ ..., # Required field, no default
84
+ description="Auto-renewal status"
85
+ )
86
+
87
+ status: SubscriptionStatus = Field(
88
+ ..., # Required field, no default
89
+ description="Subscription Status (active, trial, pending_confirmation, etc.)"
90
+ )
91
+
92
+ # IAM permissions structure
93
+ 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)"
96
+ )
97
+
98
+ fallback_plan_id: Optional[str] = Field(
99
+ ..., # Required field (can be None), no default
100
+ description="ID of the plan to fall back to if this subscription expires"
101
+ )
102
+
103
+ price_paid_usd: float = Field(
104
+ ..., # Required field, no default
105
+ description="Amount paid for this subscription in USD"
106
+ )
107
+
108
+ payment_ref: Optional[str] = Field(
109
+ default=None,
110
+ description="Reference to payment transaction"
111
+ )
112
+
113
+ # Credit management fields
114
+ subscription_based_insight_credits_per_update: int = Field(
115
+ default=0,
116
+ description="Number of insight credits to add on each update"
117
+ )
118
+
119
+ subscription_based_insight_credits_update_freq_h: int = Field(
120
+ default=24,
121
+ description="Frequency of insight credits update in hours"
122
+ )
123
+
124
+ extra_insight_credits_per_cycle: int = Field(
125
+ default=0,
126
+ description="Additional insight credits granted per subscription cycle"
127
+ )
128
+
129
+ voting_credits_per_update: int = Field(
130
+ default=0,
131
+ description="Number of voting credits to add on each update"
132
+ )
133
+
134
+ voting_credits_update_freq_h: int = Field(
135
+ default=62,
136
+ description="Frequency of voting credits update in hours"
137
+ )
138
+
139
+ # General metadata for extensibility
140
+ metadata: Dict[str, Any] = Field(
141
+ default_factory=dict,
142
+ description="Additional metadata for the subscription"
143
+ )
144
+
145
+ # Helper method to calculate cycle end date
146
+ @classmethod
147
+ def calculate_cycle_end_date(cls, start_date: datetime, validity_length: int, validity_unit: str) -> datetime:
148
+ """Calculate the end date based on start date and validity period."""
149
+ if validity_unit == "minute":
150
+ return start_date + relativedelta(minutes=validity_length)
151
+ elif validity_unit == "hour":
152
+ return start_date + relativedelta(hours=validity_length)
153
+ elif validity_unit == "day":
154
+ return start_date + relativedelta(days=validity_length)
155
+ elif validity_unit == "week":
156
+ return start_date + relativedelta(weeks=validity_length)
157
+ elif validity_unit == "year":
158
+ return start_date + relativedelta(years=validity_length)
159
+ else: # Default to months
160
+ return start_date + relativedelta(months=validity_length)
161
+
162
+ # Methods for subscription management
163
+ def is_active(self) -> bool:
164
+ """Check if the subscription is currently active."""
165
+ now = datetime.now(timezone.utc)
166
+ return (
167
+ self.status == SubscriptionStatus.ACTIVE and
168
+ self.cycle_start_date <= now <= self.cycle_end_date
169
+ )
170
+
171
+ def is_expired(self) -> bool:
172
+ """Check if the subscription has expired."""
173
+ now = datetime.now(timezone.utc)
174
+ return now > self.cycle_end_date
175
+
176
+ def days_remaining(self) -> int:
177
+ """Calculate the number of days remaining in the subscription."""
178
+ now = datetime.now(timezone.utc)
179
+ if now > self.cycle_end_date:
180
+ return 0
181
+
182
+ # Get time difference
183
+ time_diff = self.cycle_end_date - now
184
+
185
+ # If there's any time remaining but less than a day, return 1
186
+ if time_diff.days == 0 and time_diff.seconds > 0:
187
+ return 1
188
+
189
+ # Otherwise return the number of complete days
190
+ return time_diff.days
@@ -0,0 +1,125 @@
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
7
+
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 !!! #################################
12
+ class UserProfile(BaseDataModel):
13
+ """
14
+ User Profile model for storing personal information and settings.
15
+ """
16
+ model_config = ConfigDict(frozen=False, extra="forbid") # Allow field modification
17
+
18
+ # Class constants
19
+ VERSION: ClassVar[float] = 5.0 # Incremented version for primary_user_type addition
20
+ DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
21
+ OBJ_REF: ClassVar[str] = "userprofile"
22
+
23
+ schema_version: float = Field(
24
+ default=VERSION,
25
+ frozen=True,
26
+ description="Version of this Class == version of DB Schema"
27
+ )
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"
32
+ )
33
+
34
+ user_uid: str = Field(
35
+ ...,
36
+ description="User UID from Firebase Auth"
37
+ )
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
+ )
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
52
+ email: EmailStr = Field(
53
+ ...,
54
+ description="Email address",
55
+ frozen=True
56
+ )
57
+ organizations_uids: Set[str] = Field(
58
+ default_factory=set,
59
+ description="Organization UIDs the user belongs to"
60
+ )
61
+
62
+ # System identification (read-only)
63
+ provider_id: str = Field(
64
+ ...,
65
+ description="User provider ID",
66
+ frozen=True
67
+ )
68
+ aliases: Optional[Dict[str, str]] = Field(
69
+ default=None,
70
+ description="User aliases. With alias as key and description as value."
71
+ )
72
+
73
+ # 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)"
79
+ )
80
+ dob: Optional[date] = Field(
81
+ default=None,
82
+ description="Date of birth"
83
+ )
84
+ first_name: Optional[str] = Field(
85
+ default=None,
86
+ max_length=100,
87
+ description="First name"
88
+ )
89
+ last_name: Optional[str] = Field(
90
+ default=None,
91
+ max_length=100,
92
+ description="Last name"
93
+ )
94
+ mobile: Optional[str] = Field(
95
+ default=None,
96
+ pattern=r"^\+?[1-9]\d{1,14}$", # Added 'r' prefix for raw string
97
+ description="Mobile phone number"
98
+ )
99
+
100
+ metadata: Dict[str, Any] = Field(
101
+ default_factory=dict,
102
+ description="Additional metadata for the user"
103
+ )
104
+
105
+ # Remove audit fields as they're inherited from BaseDataModel
106
+
107
+ @model_validator(mode='before')
108
+ @classmethod
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