ipulse-shared-core-ftredge 5.1.1__tar.gz → 6.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 (33) hide show
  1. {ipulse_shared_core_ftredge-5.1.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-6.1.1}/PKG-INFO +1 -1
  2. ipulse_shared_core_ftredge-6.1.1/README.md +2 -0
  3. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/setup.py +1 -1
  4. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
  5. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/authorization_api.py +3 -3
  6. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +2 -2
  7. ipulse_shared_core_ftredge-5.1.1/src/ipulse_shared_core_ftredge/models/api_response.py → ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/models/base_api_response.py +2 -2
  8. ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/models/base_data_model.py +41 -0
  9. ipulse_shared_core_ftredge-5.1.1/src/ipulse_shared_core_ftredge/models/organisation.py → ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/models/organization_profile.py +36 -27
  10. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/resource_catalog_item.py +1 -1
  11. ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/models/subscription.py +60 -0
  12. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +39 -16
  13. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +4 -10
  14. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_status.py +49 -19
  15. ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +170 -0
  16. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
  17. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +3 -2
  18. ipulse_shared_core_ftredge-5.1.1/README.md +0 -2
  19. ipulse_shared_core_ftredge-5.1.1/src/ipulse_shared_core_ftredge/models/subscription.py +0 -34
  20. ipulse_shared_core_ftredge-5.1.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -75
  21. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/LICENCE +0 -0
  22. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/pyproject.toml +0 -0
  23. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/setup.cfg +0 -0
  24. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  25. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_router.py +0 -0
  26. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/database.py +0 -0
  27. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/token_validation.py +0 -0
  28. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
  29. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
  30. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/services/exceptions.py +0 -0
  31. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  32. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
  33. {ipulse_shared_core_ftredge-5.1.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 5.1.1
3
+ Version: 6.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
@@ -0,0 +1,2 @@
1
+ # ipulse_shared_core
2
+ Shared Models like User, Organization etc. Also includes shared enum_sets
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name='ipulse_shared_core_ftredge',
6
- version='5.1.1',
6
+ version='6.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=[
@@ -1,7 +1,7 @@
1
1
  # pylint: disable=missing-module-docstring
2
2
  from .models import ( UserAuth, UserProfile,Subscription,
3
3
  UserStatus, UserProfileUpdate,
4
- Organisation, StandardResponse )
4
+ OrganizationProfile, BaseAPIResponse, CustomJSONResponse )
5
5
 
6
6
 
7
7
 
@@ -182,9 +182,9 @@ async def authorizeAPIRequest(
182
182
  "usertypes": request.state.user.get("usertypes"),
183
183
  "email_verified": request.state.user.get("email_verified"),
184
184
  "iam_groups": user_status.get("iam_groups"),
185
- "active_sbscrptn_plan": user_status.get("active_sbscrptn_plan"),
186
- "active_sbscrptn_status": user_status.get("sbscrptn_status"),
187
- "sbscrptn_insight_credits": user_status.get("sbscrptn_insight_credits"),
185
+ "subscriptions": user_status.get("subscriptions"),
186
+ "sbscrptn_based_insight_credits": user_status.get("sbscrptn_based_insight_credits"),
187
+ "extra_insight_credits": user_status.get("extra_insight_credits")
188
188
  },
189
189
  "method": request.method.lower(),
190
190
  "request_resource_fields": request_resource_fields
@@ -3,8 +3,8 @@ from .subscription import Subscription
3
3
  from .user_status import UserStatus
4
4
  from .user_profile_update import UserProfileUpdate
5
5
  from .user_auth import UserAuth
6
- from .organisation import Organisation
7
- from .api_response import StandardResponse
6
+ from .organization_profile import OrganizationProfile
7
+ from .base_api_response import BaseAPIResponse, CustomJSONResponse
8
8
 
9
9
 
10
10
 
@@ -8,7 +8,7 @@ import json
8
8
 
9
9
  T = TypeVar('T')
10
10
 
11
- class StandardResponse(BaseModel, Generic[T]):
11
+ class BaseAPIResponse(BaseModel, Generic[T]):
12
12
  model_config = ConfigDict(arbitrary_types_allowed=True)
13
13
  success: bool
14
14
  data: Optional[T] = None
@@ -19,7 +19,7 @@ class StandardResponse(BaseModel, Generic[T]):
19
19
  "timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
20
20
  }
21
21
 
22
- class PaginatedResponse(StandardResponse, Generic[T]):
22
+ class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
23
23
  total_count: int
24
24
  page: int
25
25
  page_size: int
@@ -0,0 +1,41 @@
1
+ from datetime import datetime
2
+ from typing import ClassVar, Optional
3
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
4
+ import dateutil.parser
5
+
6
+ class BaseDataModel(BaseModel):
7
+ """Base model with common fields and configuration"""
8
+ model_config = ConfigDict(frozen=True, extra="forbid")
9
+
10
+ # Required class variables that must be defined in subclasses
11
+ VERSION: ClassVar[float]
12
+ DOMAIN: ClassVar[str]
13
+ OBJ_REF: ClassVar[str]
14
+
15
+ # Schema versioning
16
+ schema_version: float = Field(
17
+ ..., # Make this required
18
+ description="Version of this Class == version of DB Schema",
19
+ frozen=True
20
+ )
21
+
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(...)
27
+
28
+ @classmethod
29
+ def get_collection_name(cls) -> str:
30
+ """Generate standard collection name"""
31
+ return f"{cls.DOMAIN}_{cls.OBJ_REF}s"
32
+
33
+ @field_validator('creat_date', 'updt_date', mode='before')
34
+ @classmethod
35
+ def parse_datetime(cls, v: any) -> datetime:
36
+ if isinstance(v, datetime):
37
+ return v
38
+ try:
39
+ return dateutil.parser.isoparse(v)
40
+ except (TypeError, ValueError) as e:
41
+ raise ValueError(f"Invalid datetime format: {e}")
@@ -13,10 +13,15 @@ import uuid
13
13
  import dateutil.parser
14
14
  from ipulse_shared_base_ftredge import (
15
15
  OrganizationRelation,
16
- OrganizationIndustry
16
+ OrganizationIndustry,
17
+ Layer,
18
+ Module,
19
+ list_as_lower_strings,
20
+ Subject
17
21
  )
22
+ from .base_data_model import BaseDataModel
18
23
 
19
- class Organisation(BaseModel):
24
+ class OrganizationProfile(BaseDataModel):
20
25
  """
21
26
  Organisation model representing business entities in the system.
22
27
  Supports both retail and non-retail customer types with different validation rules.
@@ -24,30 +29,44 @@ class Organisation(BaseModel):
24
29
  model_config = ConfigDict(frozen=True, extra="forbid")
25
30
 
26
31
  # Class constants
27
- VERSION: ClassVar[float] = 1.0
28
- MODULE: ClassVar[str] = "core"
29
- CLASS_REF: ClassVar[str] = "orgn"
32
+ VERSION: ClassVar[float] = 4.1
33
+ DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.ORGANIZATION.name))
34
+ OBJ_REF: ClassVar[str] = "orgprofile"
30
35
 
31
- # Required fields
32
- puid: str = Field(
33
- default_factory=lambda: f"{datetime.utcnow().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}_{Organisation.MODULE}{Organisation.CLASS_REF}".lower(),
34
- description="Unique identifier for the organisation"
36
+ schema_version: float = Field(
37
+ default=VERSION,
38
+ description="Version of this Class == version of DB Schema",
39
+ frozen=True
35
40
  )
41
+
42
+ org_uid: str = Field(
43
+ default_factory=lambda: uuid.uuid4().hex,
44
+ description="Unique identifier for the organisation",
45
+ frozen=True
46
+ )
47
+
48
+ id: str = Field(
49
+ default=None,
50
+ description="Organisation ID, format: {OBJ_REF}_{org_uid}"
51
+ )
52
+
36
53
  name: str = Field(..., min_length=1, max_length=100)
37
54
  relations: Set[OrganizationRelation] = Field(..., description="Organisation relations/types")
38
55
 
39
- # Timestamps
40
- creat_date: datetime = Field(default_factory=datetime.utcnow)
41
- updt_date: datetime = Field(default_factory=datetime.utcnow)
42
-
43
- # Optional fields
44
- creat_by_user: Optional[str] = Field(None, max_length=100)
45
- updt_by_user: Optional[str] = Field(None, max_length=100)
46
56
  description: Optional[str] = Field(None, max_length=1000)
47
57
  industries: Optional[Set[OrganizationIndustry]] = None
48
58
  website: Optional[str] = Field(None, max_length=200)
49
59
  org_admin_user_uids: Optional[Set[str]] = None
50
60
 
61
+ @field_validator('id', mode='before')
62
+ @classmethod
63
+ def generate_id(cls, v: Optional[str], info) -> str:
64
+ values = info.data
65
+ org_uid = values.get('org_uid')
66
+ if not org_uid:
67
+ raise ValueError("org_uid must be set before generating id")
68
+ return f"{cls.OBJ_REF}_{org_uid}"
69
+
51
70
  @field_validator('relations')
52
71
  @classmethod
53
72
  def validate_relations(cls, v: Set[OrganizationRelation]) -> Set[OrganizationRelation]:
@@ -74,14 +93,4 @@ class Organisation(BaseModel):
74
93
  raise ValueError(f"{field} should not be set for retail customers")
75
94
  elif not is_retail and not v:
76
95
  raise ValueError(f"{field} required for non-retail customers")
77
- return v
78
-
79
- @field_validator('creat_date', 'updt_date', mode='before')
80
- @classmethod
81
- def parse_datetime(cls, v: any) -> datetime:
82
- if isinstance(v, datetime):
83
- return v
84
- try:
85
- return dateutil.parser.isoparse(v)
86
- except (TypeError, ValueError) as e:
87
- raise ValueError(f"Invalid datetime format: {e}")
96
+ return v
@@ -20,7 +20,7 @@
20
20
  # resr_contents:Set[str]
21
21
  # resr_original_or_processed: str
22
22
  # resr_origin: str
23
- # resr_origin_organisations_uids: Set[str]
23
+ # resr_origin_organizations_uids: Set[str]
24
24
  # resr_origin_description: str
25
25
  # resr_licences_types: Set[str]
26
26
  # resr_description_details: str
@@ -0,0 +1,60 @@
1
+ from datetime import datetime, timezone
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
6
+ from .base_data_model import BaseDataModel
7
+ # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
8
+ # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
9
+
10
+
11
+ DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlan.FREE
12
+ DEFAULT_SUBSCRIPTION_STATUS = "active"
13
+
14
+ ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
15
+ class Subscription(BaseDataModel):
16
+ """
17
+ Represents a single subscription cycle.
18
+ """
19
+
20
+ model_config = ConfigDict(frozen=True, extra="forbid")
21
+
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
+ OBJ_REF: ClassVar[str] = "subscription"
25
+
26
+ # System-managed fields (read-only)
27
+ schema_version: float = Field(
28
+ default=VERSION,
29
+ description="Version of this Class == version of DB Schema",
30
+ frozen=True
31
+ )
32
+
33
+ plan_name: SubscriptionPlan = Field(
34
+ default=DEFAULT_SUBSCRIPTION_PLAN,
35
+ description="Subscription Plan Name"
36
+ )
37
+
38
+ plan_version: float = Field(
39
+ default=1.0,
40
+ description="Version of the subscription plan"
41
+ )
42
+
43
+ cycle_start_date: datetime = Field(
44
+ default=datetime.now(timezone.utc),
45
+ description="Subscription Cycle Start Date"
46
+ )
47
+ cycle_end_date: datetime = Field(
48
+ default=lambda: datetime.now(timezone.utc) + relativedelta(years=1),
49
+ description="Subscription Cycle End Date"
50
+ )
51
+ auto_renew: bool = Field(
52
+ default=True,
53
+ description="Auto-renewal status"
54
+ )
55
+ status: str = Field(
56
+ default=DEFAULT_SUBSCRIPTION_STATUS,
57
+ description="Subscription Status (active, trial, inactive, etc.)"
58
+ )
59
+
60
+ # Remove audit fields as they're inherited from BaseDataModel
@@ -1,12 +1,14 @@
1
1
  from datetime import datetime, date
2
+ import dateutil.parser
2
3
  from typing import Set, Optional, ClassVar
3
- from pydantic import BaseModel, EmailStr, Field, ConfigDict
4
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings
4
+ from pydantic import BaseModel, EmailStr, Field, ConfigDict, field_validator
5
+ from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject
6
+ from .base_data_model import BaseDataModel
5
7
 
6
8
  # # Revision history (as model metadata)
7
9
  # CLASS_ORIGIN_AUTHOR: ClassVar[str] = "Russlan Ramdowar;russlan@ftredge.com"
8
10
  # CLASS_ORGIN_DATE: ClassVar[datetime] = datetime(2024, 1, 16, 20, 5)
9
- class UserProfile(BaseModel):
11
+ class UserProfile(BaseDataModel):
10
12
  """
11
13
  User Profile model representing user information and metadata.
12
14
  Contains both system-managed and user-editable fields.
@@ -14,8 +16,9 @@ class UserProfile(BaseModel):
14
16
  model_config = ConfigDict(frozen=True, extra="forbid")
15
17
 
16
18
  # Metadata as class variables
17
- VERSION: ClassVar[float] = 3.01
18
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name))
19
+ VERSION: ClassVar[float] = 4.1
20
+ DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
21
+ OBJ_REF: ClassVar[str] = "userprofile"
19
22
 
20
23
  # System-managed fields (read-only)
21
24
  schema_version: float = Field(
@@ -23,6 +26,18 @@ class UserProfile(BaseModel):
23
26
  description="Version of this Class == version of DB Schema",
24
27
  frozen=True
25
28
  )
29
+
30
+ id : str = Field(
31
+ ...,
32
+ description="User ID, propagated from Firebase Auth"
33
+ )
34
+
35
+ user_uid: str = Field(
36
+ ...,
37
+ description="User UID, propagated from Firebase Auth"
38
+ )
39
+
40
+
26
41
  email: EmailStr = Field(
27
42
  ...,
28
43
  description="Propagated from Firebase Auth",
@@ -30,21 +45,13 @@ class UserProfile(BaseModel):
30
45
  )
31
46
  organizations_uids: Set[str] = Field(
32
47
  default_factory=set,
33
- description="Depends on Subscription Plan, Regularly Updated",
34
- frozen=True
48
+ description="Depends on Subscription Plan, Regularly Updated"
35
49
  )
36
50
 
37
- # Timestamps and audit fields (read-only)
38
- creat_date: datetime = Field(frozen=True)
39
- creat_by_user: str = Field(frozen=True)
40
- updt_date: datetime = Field(frozen=True)
41
- updt_by_user: str = Field(frozen=True)
42
-
43
51
  # System identification (read-only)
44
52
  provider_id: str = Field(frozen=True)
45
53
  aliases: Optional[Set[str]] = Field(
46
- default=None,
47
- frozen=True
54
+ default=None
48
55
  )
49
56
 
50
57
  # User-editable fields
@@ -69,4 +76,20 @@ class UserProfile(BaseModel):
69
76
  default=None,
70
77
  pattern=r"^\+?[1-9]\d{1,14}$", # Added 'r' prefix for raw string
71
78
  description="E.164 format phone number"
72
- )
79
+ )
80
+
81
+ # Remove audit fields as they're inherited from BaseDataModel
82
+
83
+ @field_validator('id', mode='before')
84
+ @classmethod
85
+ def validate_or_generate_id(cls, v: Optional[str], info) -> str:
86
+ # If id is already provided (Firebase Auth case), return it
87
+ if v:
88
+ return v
89
+
90
+ # Fallback: generate from user_uid if needed
91
+ values = info.data
92
+ user_uid = values.get('user_uid')
93
+ if not user_uid:
94
+ raise ValueError("Either id or user_uid must be provided")
95
+ return f"{cls.OBJ_REF}_{user_uid}"
@@ -1,6 +1,6 @@
1
1
  from typing import Optional, Set, ClassVar
2
2
  from pydantic import BaseModel, Field, EmailStr, ConfigDict
3
- from datetime import date, datetime
3
+ from datetime import date
4
4
 
5
5
  class UserProfileUpdate(BaseModel):
6
6
  """
@@ -12,20 +12,12 @@ class UserProfileUpdate(BaseModel):
12
12
  # Metadata as class variables
13
13
  VERSION: ClassVar[float] = 2.01
14
14
  CLASS_ORIGIN_AUTHOR: ClassVar[str] = "Russlan Ramdowar;russlan@ftredge.com"
15
- CLASS_ORGIN_DATE: ClassVar[datetime] = datetime(2024, 3, 15, 20, 15)
16
- CLASS_REVISION_DATE: ClassVar[datetime] = datetime(2024, 3, 15, 20, 15)
15
+
17
16
 
18
17
  # System fields
19
- schema_version: Optional[float] = Field(None, description="Version of this Class == version of DB Schema")
20
18
  email: Optional[EmailStr] = Field(None, description="Propagated from Firebase Auth")
21
19
  organizations_uids: Optional[Set[str]] = Field(None, description="Organization memberships")
22
20
 
23
- # Timestamps and audit
24
- creat_date: Optional[datetime] = None
25
- creat_by_user: Optional[str] = None
26
- updt_date: Optional[datetime] = None
27
- updt_by_user: Optional[str] = None
28
-
29
21
  # System identification
30
22
  aliases: Optional[Set[str]] = None
31
23
  provider_id: Optional[str] = None
@@ -37,6 +29,8 @@ class UserProfileUpdate(BaseModel):
37
29
  last_name: Optional[str] = Field(None, max_length=100)
38
30
  mobile: Optional[str] = Field(None, pattern=r"^\+?[1-9]\d{1,14}$")
39
31
 
32
+ # Remove audit fields
33
+
40
34
  def model_dump(self, **kwargs):
41
35
  kwargs.setdefault('exclude_none', True)
42
36
  return super().model_dump(**kwargs)
@@ -1,41 +1,51 @@
1
- from datetime import datetime, timezone
2
- from dateutil.relativedelta import relativedelta
1
+ from datetime import datetime
3
2
  from typing import Set, Optional, Dict, List, ClassVar
4
- from pydantic import BaseModel, Field, ConfigDict
3
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
5
4
  from .subscription import Subscription
6
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings
5
+ from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject
6
+ import dateutil.parser
7
+ from .base_data_model import BaseDataModel
7
8
  # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
8
9
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
9
10
 
10
- DEFAULT_IAM_GROUPS={"pulseroot":["full_open_read"]}
11
- DEFAULT_SUBSCRIPTION_INSIGHT_CREDITS=10
12
- DEFAULT_EXTRA_INSIGHT_CREDITS=0
13
-
14
11
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
15
- class UserStatus(BaseModel):
12
+ class UserStatus(BaseDataModel):
16
13
  """
17
14
  User Status model for tracking user subscription and access rights.
18
15
  """
19
16
  model_config = ConfigDict(frozen=True, extra="forbid")
20
17
 
21
18
  # Class constants
22
- VERSION: ClassVar[float] = 2.3
23
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name))
24
- OBJ_REF: ClassVar[str] = "usrsttus"
19
+ VERSION: ClassVar[float] = 4.1
20
+ DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
21
+ OBJ_REF: ClassVar[str] = "userstatus"
25
22
 
26
23
  # Default values as class variables
27
24
  DEFAULT_IAM_GROUPS: ClassVar[Dict[str, List[str]]] = {"pulseroot": ["full_open_read"]}
28
25
  DEFAULT_SUBSCRIPTION_PLAN: ClassVar[str] = "subscription_free"
29
26
  DEFAULT_SUBSCRIPTION_STATUS: ClassVar[str] = "active"
30
27
  DEFAULT_SUBSCRIPTION_INSIGHT_CREDITS: ClassVar[int] = 10
28
+ DEFAULT_VOTING_CREDITS: ClassVar[int] = 0
31
29
  DEFAULT_EXTRA_INSIGHT_CREDITS: ClassVar[int] = 0
30
+
32
31
 
33
32
  # System-managed fields
34
33
  schema_version: float = Field(
35
34
  default=VERSION,
35
+ frozen=True,
36
36
  description="Version of this Class == version of DB Schema"
37
37
  )
38
38
 
39
+ id : str = Field(
40
+ ...,
41
+ description="User ID, format: {OBJ_REF}_{user_uid}"
42
+ )
43
+
44
+ user_uid: str = Field(
45
+ ...,
46
+ description="User UID from Firebase Auth"
47
+ )
48
+
39
49
  # IAM and subscription fields
40
50
  iam_groups: Dict[str, List[str]] = Field(
41
51
  default_factory=lambda: UserStatus.DEFAULT_IAM_GROUPS,
@@ -49,11 +59,11 @@ class UserStatus(BaseModel):
49
59
  )
50
60
 
51
61
  # Credits management
52
- sbscrptn_allowance_insight_credits: int = Field(
62
+ sbscrptn_based_insight_credits: int = Field(
53
63
  default_factory=lambda: UserStatus.DEFAULT_SUBSCRIPTION_INSIGHT_CREDITS,
54
64
  description="Subscription-based insight credits"
55
65
  )
56
- sbscrptn_allowance_insight_credits_updtd_on: datetime = Field(
66
+ sbscrptn_based_insight_credits_updtd_on: datetime = Field(
57
67
  default_factory=datetime.now,
58
68
  description="Last update timestamp for subscription credits"
59
69
  )
@@ -61,12 +71,32 @@ class UserStatus(BaseModel):
61
71
  default_factory=lambda: UserStatus.DEFAULT_EXTRA_INSIGHT_CREDITS,
62
72
  description="Additional purchased insight credits (non-expiring)"
63
73
  )
74
+
75
+ extra_insight_credits_updtd_on: datetime = Field(
76
+ default_factory=datetime.now,
77
+ description="Last update timestamp for extra credits"
78
+ )
79
+
80
+ voting_credits: int = Field(
81
+ default=lambda : UserStatus.DEFAULT_VOTING_CREDITS,
82
+ description="Voting credits for user"
83
+ )
64
84
 
65
85
  # Optional fields
66
86
  payment_refs_uids: Optional[Set[str]] = None
67
87
 
68
- # Audit fields
69
- creat_date: datetime
70
- creat_by_user: str
71
- updt_date: datetime
72
- updt_by_user: str
88
+ # Remove audit fields as they're inherited from BaseDataModel
89
+
90
+ @field_validator('id', mode='before')
91
+ @classmethod
92
+ def validate_or_generate_id(cls, v: Optional[str], info) -> str:
93
+ # If id is already provided (Firebase Auth case), return it
94
+ if v:
95
+ return v
96
+
97
+ # Fallback: generate from user_uid if needed
98
+ values = info.data
99
+ user_uid = values.get('user_uid')
100
+ if not user_uid:
101
+ raise ValueError("Either id or user_uid must be provided")
102
+ return f"{cls.OBJ_REF}_{user_uid}"
@@ -0,0 +1,170 @@
1
+ from typing import Dict, Any, Optional, List, TypeVar, Generic
2
+ import logging
3
+ from datetime import datetime, timezone
4
+ from pydantic import BaseModel
5
+ from google.cloud import firestore
6
+ from .exceptions import ResourceNotFoundError, ValidationError, ServiceError
7
+
8
+ T = TypeVar('T', bound=BaseModel)
9
+
10
+ class BaseFirestoreService(Generic[T]):
11
+ """Base class for Firestore services with common CRUD operations"""
12
+
13
+ def __init__(self, db: firestore.Client, collection_name: str, resource_type: str, logger: logging.Logger):
14
+ self.db = db
15
+ self.collection_name = collection_name
16
+ self.resource_type = resource_type
17
+ self.logger = logger
18
+
19
+ async def create_document(self, doc_id: str, data: T, creator_uid: str) -> Dict[str, Any]:
20
+ """Standard create method with audit fields"""
21
+ try:
22
+ current_time = datetime.now(timezone.utc)
23
+ doc_data = data.model_dump(mode='json')
24
+
25
+ # Add audit fields
26
+ doc_data.update({
27
+ 'creat_date': current_time.isoformat(),
28
+ 'creat_by_user': creator_uid,
29
+ 'updt_date': current_time.isoformat(),
30
+ 'updt_by_user': creator_uid
31
+ })
32
+
33
+ doc_ref = self.db.collection(self.collection_name).document(doc_id)
34
+ doc_ref.set(doc_data)
35
+
36
+ self.logger.info(f"Created {self.resource_type}: {doc_id}")
37
+ return doc_data
38
+
39
+ except Exception as e:
40
+ self.logger.error(f"Error creating {self.resource_type}: {e}", exc_info=True)
41
+ raise ServiceError(
42
+ operation=f"creating {self.resource_type}",
43
+ error=e,
44
+ resource_type=self.resource_type,
45
+ resource_id=doc_id
46
+ ) from e
47
+
48
+ async def create_batch_documents(self, documents: List[T], creator_uid: str) -> List[Dict[str, Any]]:
49
+ """Standard batch create method"""
50
+ try:
51
+ batch = self.db.batch()
52
+ current_time = datetime.now(timezone.utc)
53
+ created_docs = []
54
+
55
+ for doc in documents:
56
+ doc_data = doc.model_dump(mode='json')
57
+ doc_data.update({
58
+ 'creat_date': current_time.isoformat(),
59
+ 'creat_by_user': creator_uid,
60
+ 'updt_date': current_time.isoformat(),
61
+ 'updt_by_user': creator_uid
62
+ })
63
+
64
+ doc_ref = self.db.collection(self.collection_name).document(doc_data.get('id'))
65
+ batch.set(doc_ref, doc_data)
66
+ created_docs.append(doc_data)
67
+
68
+ batch.commit()
69
+ self.logger.info(f"Created {len(documents)} {self.resource_type}s in batch")
70
+ return created_docs
71
+
72
+ except Exception as e:
73
+ self.logger.error(f"Error batch creating {self.resource_type}s: {e}", exc_info=True)
74
+ raise ServiceError(
75
+ operation=f"batch creating {self.resource_type}s",
76
+ error=e,
77
+ resource_type=self.resource_type,
78
+ resource_id=doc_data.get('id')
79
+ ) from e
80
+
81
+ async def get_document(self, doc_id: str) -> Dict[str, Any]:
82
+ """Get a document by ID with standardized error handling"""
83
+ doc_ref = self.db.collection(self.collection_name).document(doc_id)
84
+ doc = doc_ref.get()
85
+
86
+ if not doc.exists:
87
+ raise ResourceNotFoundError(
88
+ resource_type=self.resource_type,
89
+ resource_id=doc_id,
90
+ additional_info={"collection": self.collection_name}
91
+ )
92
+
93
+ return doc.to_dict()
94
+
95
+ async def update_document(self, doc_id: str, update_data: Dict[str, Any], updater_uid: str) -> Dict[str, Any]:
96
+ """Standard update method with validation and audit fields"""
97
+ try:
98
+ doc_ref = self.db.collection(self.collection_name).document(doc_id)
99
+
100
+ if not doc_ref.get().exists:
101
+ raise ResourceNotFoundError(
102
+ resource_type=self.resource_type,
103
+ resource_id=doc_id,
104
+ additional_info={"collection": self.collection_name}
105
+ )
106
+
107
+ valid_fields = self._validate_update_fields(update_data)
108
+
109
+ # Add audit fields
110
+ valid_fields.update({
111
+ 'updt_date': datetime.now(timezone.utc).isoformat(),
112
+ 'updt_by_user': updater_uid
113
+ })
114
+
115
+ doc_ref.update(valid_fields)
116
+ return doc_ref.get().to_dict()
117
+
118
+ except (ResourceNotFoundError, ValidationError):
119
+ raise
120
+ except Exception as e:
121
+ self.logger.error(f"Error updating {self.resource_type}: {e}", exc_info=True)
122
+ raise ServiceError(
123
+ operation=f"updating {self.resource_type}",
124
+ error=e,
125
+ resource_type=self.resource_type,
126
+ resource_id=doc_id
127
+ ) from e
128
+
129
+ async def delete_document(self, doc_id: str) -> None:
130
+ """Standard delete method"""
131
+ try:
132
+ doc_ref = self.db.collection(self.collection_name).document(doc_id)
133
+ if not doc_ref.get().exists:
134
+ raise ResourceNotFoundError(
135
+ resource_type=self.resource_type,
136
+ resource_id=doc_id
137
+ )
138
+
139
+ doc_ref.delete()
140
+ self.logger.info(f"Deleted {self.resource_type}: {doc_id}")
141
+
142
+ except ResourceNotFoundError:
143
+ raise
144
+ except Exception as e:
145
+ self.logger.error(f"Error deleting {self.resource_type}: {e}", exc_info=True)
146
+ raise ServiceError(
147
+ operation=f"deleting {self.resource_type}",
148
+ error=e,
149
+ resource_type=self.resource_type,
150
+ resource_id=doc_id
151
+ ) from e
152
+
153
+ def _validate_update_fields(self, update_data: Dict[str, Any]) -> Dict[str, Any]:
154
+ """Centralized update fields validation"""
155
+ if not isinstance(update_data, dict):
156
+ update_data = update_data.model_dump(exclude_unset=True)
157
+
158
+ valid_fields = {
159
+ k: v for k, v in update_data.items()
160
+ if v is not None and not (isinstance(v, (list, dict, set)) and len(v) == 0)
161
+ }
162
+
163
+ if not valid_fields:
164
+ raise ValidationError(
165
+ resource_type=self.resource_type,
166
+ detail="No valid fields to update",
167
+ resource_id=None
168
+ )
169
+
170
+ return valid_fields
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 5.1.1
3
+ Version: 6.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
@@ -14,8 +14,9 @@ src/ipulse_shared_core_ftredge/dependencies/authorization_api.py
14
14
  src/ipulse_shared_core_ftredge/dependencies/database.py
15
15
  src/ipulse_shared_core_ftredge/dependencies/token_validation.py
16
16
  src/ipulse_shared_core_ftredge/models/__init__.py
17
- src/ipulse_shared_core_ftredge/models/api_response.py
18
- src/ipulse_shared_core_ftredge/models/organisation.py
17
+ src/ipulse_shared_core_ftredge/models/base_api_response.py
18
+ src/ipulse_shared_core_ftredge/models/base_data_model.py
19
+ src/ipulse_shared_core_ftredge/models/organization_profile.py
19
20
  src/ipulse_shared_core_ftredge/models/resource_catalog_item.py
20
21
  src/ipulse_shared_core_ftredge/models/subscription.py
21
22
  src/ipulse_shared_core_ftredge/models/user_auth.py
@@ -1,2 +0,0 @@
1
- # ipulse_shared_core
2
- Shared Models like User, Organisation etc. Also includes shared enum_sets
@@ -1,34 +0,0 @@
1
- from datetime import datetime, timezone
2
- from dateutil.relativedelta import relativedelta
3
- from typing import Set, Optional, Dict, List, ClassVar
4
- from pydantic import BaseModel, Field, ConfigDict
5
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings
6
- # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
7
- # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
8
-
9
-
10
- DEFAULT_SUBSCRIPTION_PLAN="subscription_free"
11
- DEFAULT_SUBSCRIPTION_STATUS="active"
12
-
13
- ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
14
- class Subscription(BaseModel):
15
- """
16
- Represents a single subscription cycle.
17
- """
18
- plan_name: str = Field(
19
- default=DEFAULT_SUBSCRIPTION_PLAN,
20
- description="Subscription Plan Name"
21
- )
22
-
23
- cycle_start_date: datetime = Field(
24
- default=datetime.now(timezone.utc),
25
- description="Subscription Cycle Start Date"
26
- )
27
- cycle_end_date: datetime = Field(
28
- default=lambda: datetime.now(timezone.utc) + relativedelta(years=1),
29
- description="Subscription Cycle End Date"
30
- )
31
- status: str = Field(
32
- default=DEFAULT_SUBSCRIPTION_STATUS,
33
- description="Subscription Status (active, inactive, etc.)"
34
- )
@@ -1,75 +0,0 @@
1
- from typing import Dict, Any, Optional
2
- from datetime import datetime,timezone
3
- from fastapi import HTTPException
4
- from google.cloud import firestore
5
- from .exceptions import ResourceNotFoundError, ValidationError
6
-
7
- class BaseFirestoreService:
8
- def __init__(self, db: firestore.Client, collection_name: str, resource_type: str):
9
- self.db = db
10
- self.collection_name = collection_name
11
- self.resource_type = resource_type
12
-
13
- def _validate_update_fields(self, update_data: Dict[str, Any]) -> Dict[str, Any]:
14
- """Centralized update fields validation"""
15
- if not isinstance(update_data, dict):
16
- update_data = update_data.model_dump(exclude_unset=True)
17
-
18
- valid_fields = {
19
- k: v for k, v in update_data.items()
20
- if v is not None and not (isinstance(v, (list, dict, set)) and len(v) == 0)
21
- }
22
-
23
- if not valid_fields:
24
- raise ValidationError(
25
- resource_type=self.resource_type,
26
- detail="No valid fields to update",
27
- resource_id=None
28
- )
29
-
30
- return valid_fields
31
-
32
- async def get_document(self, doc_id: str) -> Dict[str, Any]:
33
- """Get a document by ID with standardized error handling"""
34
- doc_ref = self.db.collection(self.collection_name).document(doc_id)
35
- doc = doc_ref.get()
36
-
37
- if not doc.exists:
38
- raise ResourceNotFoundError(
39
- resource_type=self.resource_type,
40
- resource_id=doc_id,
41
- additional_info={"collection": self.collection_name}
42
- )
43
-
44
- return doc.to_dict()
45
-
46
- async def update_document(self, doc_id: str, update_data: Dict[str, Any], user_uid: Optional[str] = None) -> Dict[str, Any]:
47
- """Standard update method with validation and audit fields"""
48
- try:
49
- doc_ref = self.db.collection(self.collection_name).document(doc_id)
50
-
51
- if not doc_ref.get().exists:
52
- raise ResourceNotFoundError(
53
- resource_type=self.resource_type,
54
- resource_id=doc_id,
55
- additional_info={"collection": self.collection_name}
56
- )
57
-
58
- valid_fields = self._validate_update_fields(update_data)
59
-
60
- # Add audit fields
61
- valid_fields.update({
62
- 'updt_date': datetime.now(timezone.utc).isoformat(),
63
- 'updt_by_user': user_uid if user_uid else None
64
- })
65
-
66
- doc_ref.update(valid_fields)
67
- return doc_ref.get().to_dict()
68
-
69
- except (ResourceNotFoundError, ValidationError):
70
- raise
71
- except Exception as e:
72
- raise HTTPException(
73
- status_code=500,
74
- detail=f"Failed to update {self.resource_type}: {str(e)}"
75
- )