ipulse-shared-core-ftredge 5.2.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 (32) hide show
  1. {ipulse_shared_core_ftredge-5.2.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.2.1 → ipulse_shared_core_ftredge-6.1.1}/setup.py +1 -1
  4. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
  5. {ipulse_shared_core_ftredge-5.2.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.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +2 -2
  7. ipulse_shared_core_ftredge-5.2.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.2.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 +5 -21
  10. {ipulse_shared_core_ftredge-5.2.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-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/subscription.py +20 -7
  12. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +22 -15
  13. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +2 -14
  14. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_status.py +38 -14
  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.2.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.2.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.2.1/README.md +0 -2
  19. ipulse_shared_core_ftredge-5.2.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -75
  20. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/LICENCE +0 -0
  21. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/pyproject.toml +0 -0
  22. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/setup.cfg +0 -0
  23. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  24. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_router.py +0 -0
  25. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/database.py +0 -0
  26. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/token_validation.py +0 -0
  27. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
  28. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
  29. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/services/exceptions.py +0 -0
  30. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  31. {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
  32. {ipulse_shared_core_ftredge-5.2.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.2.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.2.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}")
@@ -17,10 +17,11 @@ from ipulse_shared_base_ftredge import (
17
17
  Layer,
18
18
  Module,
19
19
  list_as_lower_strings,
20
- Sector
20
+ Subject
21
21
  )
22
+ from .base_data_model import BaseDataModel
22
23
 
23
- class Organisation(BaseModel):
24
+ class OrganizationProfile(BaseDataModel):
24
25
  """
25
26
  Organisation model representing business entities in the system.
26
27
  Supports both retail and non-retail customer types with different validation rules.
@@ -29,7 +30,7 @@ class Organisation(BaseModel):
29
30
 
30
31
  # Class constants
31
32
  VERSION: ClassVar[float] = 4.1
32
- MODULE: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Sector.USERCORE.name))
33
+ DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.ORGANIZATION.name))
33
34
  OBJ_REF: ClassVar[str] = "orgprofile"
34
35
 
35
36
  schema_version: float = Field(
@@ -52,13 +53,6 @@ class Organisation(BaseModel):
52
53
  name: str = Field(..., min_length=1, max_length=100)
53
54
  relations: Set[OrganizationRelation] = Field(..., description="Organisation relations/types")
54
55
 
55
- # Timestamps
56
- creat_date: datetime = Field(default_factory=datetime.utcnow)
57
- updt_date: datetime = Field(default_factory=datetime.utcnow)
58
-
59
- # Optional fields
60
- creat_by_user: Optional[str] = Field(None, max_length=100)
61
- updt_by_user: Optional[str] = Field(None, max_length=100)
62
56
  description: Optional[str] = Field(None, max_length=1000)
63
57
  industries: Optional[Set[OrganizationIndustry]] = None
64
58
  website: Optional[str] = Field(None, max_length=200)
@@ -99,14 +93,4 @@ class Organisation(BaseModel):
99
93
  raise ValueError(f"{field} should not be set for retail customers")
100
94
  elif not is_retail and not v:
101
95
  raise ValueError(f"{field} required for non-retail customers")
102
- return v
103
-
104
- @field_validator('creat_date', 'updt_date', mode='before')
105
- @classmethod
106
- def parse_datetime(cls, v: any) -> datetime:
107
- if isinstance(v, datetime):
108
- return v
109
- try:
110
- return dateutil.parser.isoparse(v)
111
- except (TypeError, ValueError) as e:
112
- 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
@@ -1,8 +1,9 @@
1
1
  from datetime import datetime, timezone
2
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, Sector, SubscriptionPlan
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
6
7
  # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
7
8
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
8
9
 
@@ -11,7 +12,7 @@ DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlan.FREE
11
12
  DEFAULT_SUBSCRIPTION_STATUS = "active"
12
13
 
13
14
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
14
- class Subscription(BaseModel):
15
+ class Subscription(BaseDataModel):
15
16
  """
16
17
  Represents a single subscription cycle.
17
18
  """
@@ -19,7 +20,8 @@ class Subscription(BaseModel):
19
20
  model_config = ConfigDict(frozen=True, extra="forbid")
20
21
 
21
22
  VERSION: ClassVar[float] = 1.1
22
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Sector.LOOKUP.name))
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"
23
25
 
24
26
  # System-managed fields (read-only)
25
27
  schema_version: float = Field(
@@ -33,6 +35,11 @@ class Subscription(BaseModel):
33
35
  description="Subscription Plan Name"
34
36
  )
35
37
 
38
+ plan_version: float = Field(
39
+ default=1.0,
40
+ description="Version of the subscription plan"
41
+ )
42
+
36
43
  cycle_start_date: datetime = Field(
37
44
  default=datetime.now(timezone.utc),
38
45
  description="Subscription Cycle Start Date"
@@ -41,7 +48,13 @@ class Subscription(BaseModel):
41
48
  default=lambda: datetime.now(timezone.utc) + relativedelta(years=1),
42
49
  description="Subscription Cycle End Date"
43
50
  )
51
+ auto_renew: bool = Field(
52
+ default=True,
53
+ description="Auto-renewal status"
54
+ )
44
55
  status: str = Field(
45
56
  default=DEFAULT_SUBSCRIPTION_STATUS,
46
- description="Subscription Status (active, inactive, etc.)"
47
- )
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, Sector
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.
@@ -15,7 +17,8 @@ class UserProfile(BaseModel):
15
17
 
16
18
  # Metadata as class variables
17
19
  VERSION: ClassVar[float] = 4.1
18
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Sector.USERCORE.name))
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(
@@ -45,12 +48,6 @@ class UserProfile(BaseModel):
45
48
  description="Depends on Subscription Plan, Regularly Updated"
46
49
  )
47
50
 
48
- # Timestamps and audit fields (read-only)
49
- creat_date: datetime = Field(frozen=True)
50
- creat_by_user: str = Field(frozen=True)
51
- updt_date: datetime = Field(frozen=True)
52
- updt_by_user: str = Field(frozen=True)
53
-
54
51
  # System identification (read-only)
55
52
  provider_id: str = Field(frozen=True)
56
53
  aliases: Optional[Set[str]] = Field(
@@ -81,8 +78,18 @@ class UserProfile(BaseModel):
81
78
  description="E.164 format phone number"
82
79
  )
83
80
 
84
- # Audit fields
85
- creat_date: datetime = Field(default_factory=datetime.now)
86
- creat_by_user: str = Field(frozen=True)
87
- updt_date: datetime = Field(default_factory=datetime.now)
88
- updt_by_user: str = Field(frozen=True)
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,10 +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
4
-
5
- # CLASS_ORGIN_DATE: ClassVar[datetime] = datetime(2024, 3, 15, 20, 15)
6
- # CLASS_REVISION_DATE: ClassVar[datetime] = datetime(2024, 3, 15, 20, 15)
7
-
3
+ from datetime import date
8
4
 
9
5
  class UserProfileUpdate(BaseModel):
10
6
  """
@@ -22,12 +18,6 @@ class UserProfileUpdate(BaseModel):
22
18
  email: Optional[EmailStr] = Field(None, description="Propagated from Firebase Auth")
23
19
  organizations_uids: Optional[Set[str]] = Field(None, description="Organization memberships")
24
20
 
25
- # Timestamps and audit
26
- creat_date: Optional[datetime] = None
27
- creat_by_user: Optional[str] = None
28
- updt_date: Optional[datetime] = None
29
- updt_by_user: Optional[str] = None
30
-
31
21
  # System identification
32
22
  aliases: Optional[Set[str]] = None
33
23
  provider_id: Optional[str] = None
@@ -39,9 +29,7 @@ class UserProfileUpdate(BaseModel):
39
29
  last_name: Optional[str] = Field(None, max_length=100)
40
30
  mobile: Optional[str] = Field(None, pattern=r"^\+?[1-9]\d{1,14}$")
41
31
 
42
-
43
-
44
-
32
+ # Remove audit fields
45
33
 
46
34
  def model_dump(self, **kwargs):
47
35
  kwargs.setdefault('exclude_none', True)
@@ -1,13 +1,15 @@
1
1
  from datetime import datetime
2
2
  from typing import Set, Optional, Dict, List, ClassVar
3
- from pydantic import BaseModel, Field, ConfigDict
3
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
4
4
  from .subscription import Subscription
5
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Sector
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
6
8
  # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
7
9
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
8
10
 
9
11
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
10
- class UserStatus(BaseModel):
12
+ class UserStatus(BaseDataModel):
11
13
  """
12
14
  User Status model for tracking user subscription and access rights.
13
15
  """
@@ -15,7 +17,7 @@ class UserStatus(BaseModel):
15
17
 
16
18
  # Class constants
17
19
  VERSION: ClassVar[float] = 4.1
18
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Sector.USERCORE.name))
20
+ DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
19
21
  OBJ_REF: ClassVar[str] = "userstatus"
20
22
 
21
23
  # Default values as class variables
@@ -23,23 +25,25 @@ class UserStatus(BaseModel):
23
25
  DEFAULT_SUBSCRIPTION_PLAN: ClassVar[str] = "subscription_free"
24
26
  DEFAULT_SUBSCRIPTION_STATUS: ClassVar[str] = "active"
25
27
  DEFAULT_SUBSCRIPTION_INSIGHT_CREDITS: ClassVar[int] = 10
28
+ DEFAULT_VOTING_CREDITS: ClassVar[int] = 0
26
29
  DEFAULT_EXTRA_INSIGHT_CREDITS: ClassVar[int] = 0
27
-
30
+
28
31
 
29
32
  # System-managed fields
30
33
  schema_version: float = Field(
31
34
  default=VERSION,
35
+ frozen=True,
32
36
  description="Version of this Class == version of DB Schema"
33
37
  )
34
38
 
35
39
  id : str = Field(
36
40
  ...,
37
- description="User ID, propagated from Firebase Auth"
41
+ description="User ID, format: {OBJ_REF}_{user_uid}"
38
42
  )
39
43
 
40
44
  user_uid: str = Field(
41
45
  ...,
42
- description="User UID, propagated from Firebase Auth"
46
+ description="User UID from Firebase Auth"
43
47
  )
44
48
 
45
49
  # IAM and subscription fields
@@ -55,11 +59,11 @@ class UserStatus(BaseModel):
55
59
  )
56
60
 
57
61
  # Credits management
58
- sbscrptn_allowance_insight_credits: int = Field(
62
+ sbscrptn_based_insight_credits: int = Field(
59
63
  default_factory=lambda: UserStatus.DEFAULT_SUBSCRIPTION_INSIGHT_CREDITS,
60
64
  description="Subscription-based insight credits"
61
65
  )
62
- sbscrptn_allowance_insight_credits_updtd_on: datetime = Field(
66
+ sbscrptn_based_insight_credits_updtd_on: datetime = Field(
63
67
  default_factory=datetime.now,
64
68
  description="Last update timestamp for subscription credits"
65
69
  )
@@ -67,12 +71,32 @@ class UserStatus(BaseModel):
67
71
  default_factory=lambda: UserStatus.DEFAULT_EXTRA_INSIGHT_CREDITS,
68
72
  description="Additional purchased insight credits (non-expiring)"
69
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
+ )
70
84
 
71
85
  # Optional fields
72
86
  payment_refs_uids: Optional[Set[str]] = None
73
87
 
74
- # Audit fields
75
- creat_date: datetime = Field(default_factory=datetime.now)
76
- creat_by_user: str = Field(frozen=True)
77
- updt_date: datetime = Field(default_factory=datetime.now)
78
- updt_by_user: str = Field(frozen=True)
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.2.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,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
- )