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.
- {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
- ipulse_shared_core_ftredge-6.1.1/README.md +2 -0
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/setup.py +1 -1
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
- {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
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +2 -2
- 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
- ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/models/base_data_model.py +41 -0
- 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
- {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
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/models/subscription.py +20 -7
- {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
- {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
- {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
- ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +170 -0
- {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
- {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
- ipulse_shared_core_ftredge-5.2.1/README.md +0 -2
- ipulse_shared_core_ftredge-5.2.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -75
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {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
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/dependencies/database.py +0 -0
- {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
- {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
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
- {ipulse_shared_core_ftredge-5.2.1 → ipulse_shared_core_ftredge-6.1.1}/src/ipulse_shared_core_ftredge/services/exceptions.py +0 -0
- {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
- {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
- {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:
|
|
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
|
|
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|
|
3
3
|
|
|
4
4
|
setup(
|
|
5
5
|
name='ipulse_shared_core_ftredge',
|
|
6
|
-
version='
|
|
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=[
|
|
@@ -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
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
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 .
|
|
7
|
-
from .
|
|
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
|
|
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
|
|
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
|
-
|
|
20
|
+
Subject
|
|
21
21
|
)
|
|
22
|
+
from .base_data_model import BaseDataModel
|
|
22
23
|
|
|
23
|
-
class
|
|
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
|
-
|
|
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
|
-
#
|
|
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,
|
|
4
|
-
from pydantic import
|
|
5
|
-
from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings,
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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}"
|
ipulse_shared_core_ftredge-6.1.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py
ADDED
|
@@ -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:
|
|
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/
|
|
18
|
-
src/ipulse_shared_core_ftredge/models/
|
|
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
|
ipulse_shared_core_ftredge-5.2.1/src/ipulse_shared_core_ftredge/services/base_firestore_service.py
DELETED
|
@@ -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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|