ipulse-shared-core-ftredge 27.1.1__tar.gz → 27.6.2__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.
Files changed (64) hide show
  1. {ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-27.6.2}/PKG-INFO +2 -2
  2. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/setup.py +2 -2
  3. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +6 -1
  4. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -1
  5. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/base_api_response.py +4 -0
  6. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/models/base_data_model.py → ipulse_shared_core_ftredge-27.6.2/src/ipulse_shared_core_ftredge/models/base_nosql_model.py +34 -10
  7. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +7 -13
  8. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/catalog/usertype.py +7 -12
  9. ipulse_shared_core_ftredge-27.6.2/src/ipulse_shared_core_ftredge/models/time_series_packaged_dataset_model.py +44 -0
  10. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/user/user_subscription.py +7 -11
  11. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/user/userprofile.py +8 -11
  12. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/user/userstatus.py +7 -14
  13. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/__init__.py +1 -1
  14. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/base/__init__.py +1 -0
  15. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/base/cache_aware_firestore_service.py +67 -6
  16. ipulse_shared_core_ftredge-27.6.2/src/ipulse_shared_core_ftredge/services/base/multi_collection_cache_aware_firestore_service.py +244 -0
  17. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +11 -0
  18. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/charging_processors.py +2 -2
  19. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +2 -2
  20. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +4 -4
  21. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -1
  22. ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/utils/authz_credit_extraction.py +0 -0
  23. ipulse_shared_core_ftredge-27.1.1/tests/test_shared_cache.py +0 -147
  24. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/LICENCE +0 -0
  25. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/README.md +0 -0
  26. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/pyproject.toml +0 -0
  27. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/setup.cfg +0 -0
  28. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
  29. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
  30. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +0 -0
  31. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  32. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
  33. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  34. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py +0 -0
  35. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
  36. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
  37. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +0 -0
  38. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +0 -0
  39. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/catalog/__init__.py +0 -0
  40. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/credit_api_response.py +0 -0
  41. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/custom_json_response.py +0 -0
  42. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/user/__init__.py +0 -0
  43. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/user/user_permissions.py +0 -0
  44. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/user/userauth.py +0 -0
  45. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -0
  46. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +0 -0
  47. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +0 -0
  48. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/catalog/__init__.py +0 -0
  49. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +0 -0
  50. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/__init__.py +0 -0
  51. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/user_charging_operations.py +0 -0
  52. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +0 -0
  53. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +0 -0
  54. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +0 -0
  55. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +0 -0
  56. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/userauth_operations.py +0 -0
  57. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/userprofile_operations.py +0 -0
  58. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user/userstatus_operations.py +0 -0
  59. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/user_charging_service.py +0 -0
  60. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  61. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
  62. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
  63. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  64. {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 27.1.1
3
+ Version: 27.6.2
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
@@ -12,7 +12,7 @@ License-File: LICENCE
12
12
  Requires-Dist: pydantic[email]~=2.5
13
13
  Requires-Dist: python-dateutil~=2.8
14
14
  Requires-Dist: fastapi~=0.115.8
15
- Requires-Dist: ipulse_shared_base_ftredge==11.1.1
15
+ Requires-Dist: ipulse_shared_base_ftredge~=12.6.0
16
16
  Dynamic: author
17
17
  Dynamic: classifier
18
18
  Dynamic: home-page
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name='ipulse_shared_core_ftredge',
6
- version='27.1.1',
6
+ version='27.6.2',
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=[
@@ -11,7 +11,7 @@ setup(
11
11
  'pydantic[email]~=2.5',
12
12
  'python-dateutil~=2.8',
13
13
  'fastapi~=0.115.8',
14
- 'ipulse_shared_base_ftredge==11.1.1',
14
+ 'ipulse_shared_base_ftredge~=12.6.0',
15
15
  ],
16
16
  author='Russlan Ramdowar',
17
17
  description='Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.',
@@ -110,8 +110,13 @@ async def get_userstatus(
110
110
  else:
111
111
  raise ValueError(f"Expected UserStatus object or dict, got {type(status_obj)}")
112
112
 
113
+ except ResourceNotFoundError:
114
+ # Let ResourceNotFoundError bubble up as 404 - this is a user issue, not a server error
115
+ log.warning(f"User status not found for user {user_uid} during authorization")
116
+ raise
113
117
  except Exception as e:
114
- log.error(f"Error fetching user status via UserCoreService: {str(e)}")
118
+ # Only wrap true service errors (database failures, network issues, etc) in ServiceError
119
+ log.error(f"Service error fetching user status via UserCoreService: {str(e)}")
115
120
  raise ServiceError(
116
121
  operation="fetching user status for authz via UserCoreService",
117
122
  error=e,
@@ -1,4 +1,4 @@
1
- from .base_data_model import BaseDataModel
1
+ from .base_nosql_model import BaseNoSQLModel
2
2
  from .base_api_response import BaseAPIResponse, PaginatedAPIResponse
3
3
  from .credit_api_response import CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
4
4
  from .custom_json_response import CustomJSONResponse
@@ -16,6 +16,10 @@ class BaseAPIResponse(BaseModel, Generic[T]):
16
16
  message: Optional[str] = None
17
17
  error: Optional[str] = None
18
18
 
19
+ # Optional fields for specific use cases
20
+ cache_hit: Optional[bool] = None # Whether data came from cache
21
+ charged: Optional[bool] = None # Whether credits were charged for this request
22
+
19
23
  metadata: Dict[str, Any] = {
20
24
  "timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
21
25
  }
@@ -1,31 +1,55 @@
1
1
  from datetime import datetime, timezone
2
- from typing import Any
3
- from typing import ClassVar
4
- from pydantic import BaseModel, Field, ConfigDict, field_validator
2
+ from typing import Any , Optional, ClassVar
3
+ from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator
5
4
  import dateutil.parser
6
5
 
7
- class BaseDataModel(BaseModel):
6
+ class BaseNoSQLModel(BaseModel):
8
7
  """Base model with common fields and configuration"""
9
8
  model_config = ConfigDict(frozen=False, extra="forbid")
10
9
 
11
10
  # Required class variables that must be defined in subclasses
12
- VERSION: ClassVar[float]
11
+ SCHEMA_ID: ClassVar[str]
12
+ SCHEMA_NAME: ClassVar[str]
13
+ VERSION: ClassVar[int]
13
14
  DOMAIN: ClassVar[str]
14
15
  OBJ_REF: ClassVar[str]
15
16
 
16
- # Schema versioning
17
- schema_version: float = Field(
18
- ..., # Make this required
17
+ # Schema versioning - these will be auto-populated from class variables
18
+ schema_version: int = Field(
19
+ default=None, # Will be auto-populated by model_validator
19
20
  description="Version of this Class == version of DB Schema",
20
21
  frozen=True # Keep schema version frozen for data integrity
21
22
  )
22
23
 
24
+ schema_id: str = Field(
25
+ default=None, # Will be auto-populated by model_validator
26
+ description="Identifier for the schema this document adheres to"
27
+ )
28
+ schema_name: str = Field(
29
+ default=None, # Will be auto-populated by model_validator
30
+ description="Name of the schema this document adheres to"
31
+ )
32
+
23
33
  # Audit fields - created fields are frozen after creation, updated fields are mutable
24
- created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
25
- created_by: str = Field(..., frozen=True)
34
+ created_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
35
+ created_by: Optional[str] = Field(..., frozen=True)
26
36
  updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
27
37
  updated_by: str = Field(...)
28
38
 
39
+ @model_validator(mode='before')
40
+ @classmethod
41
+ def populate_schema_fields(cls, values):
42
+ """Auto-populate schema fields from class variables if not provided"""
43
+ if isinstance(values, dict):
44
+ # Set if not already provided or if None
45
+ if ('schema_version' not in values or values.get('schema_version') is None) and hasattr(cls, 'VERSION'):
46
+ values['schema_version'] = cls.VERSION
47
+ if ('schema_id' not in values or values.get('schema_id') is None) and hasattr(cls, 'SCHEMA_ID'):
48
+ values['schema_id'] = cls.SCHEMA_ID
49
+ if ('schema_name' not in values or values.get('schema_name') is None) and hasattr(cls, 'SCHEMA_NAME'):
50
+ values['schema_name'] = cls.SCHEMA_NAME
51
+ return values
52
+
29
53
  @classmethod
30
54
  def get_collection_name(cls) -> str:
31
55
  """Generate standard collection name"""
@@ -10,9 +10,9 @@ from enum import StrEnum
10
10
  from datetime import datetime, timezone, timedelta
11
11
  from pydantic import Field, ConfigDict, field_validator,model_validator, BaseModel
12
12
  from ipulse_shared_base_ftredge import (Layer, Module, list_enums_as_lower_strings,
13
- Subject, SubscriptionPlanName,ObjectOverallStatus,
13
+ SystemSubject, SubscriptionPlanName,ObjectOverallStatus,
14
14
  SubscriptionStatus, TimeUnit)
15
- from ..base_data_model import BaseDataModel
15
+ from ..base_nosql_model import BaseNoSQLModel
16
16
  from ..user.user_permissions import UserPermission
17
17
 
18
18
 
@@ -51,25 +51,19 @@ class PlanUpgradePath(BaseModel):
51
51
 
52
52
 
53
53
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION IF SCHEMA IS BEING MODIFIED !!! ############################################
54
- class SubscriptionPlan(BaseDataModel):
54
+ class SubscriptionPlan(BaseNoSQLModel):
55
55
  """
56
56
  Configuration template for subscription plans stored in Firestore.
57
57
  These templates define the default settings applied when creating user subscriptions.
58
58
  """
59
59
 
60
60
  model_config = ConfigDict(extra="forbid")
61
-
62
- VERSION: ClassVar[float] = 1.0
63
- DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.CATALOG))
61
+ SCHEMA_ID: ClassVar[str] = ""
62
+ SCHEMA_NAME: ClassVar[str] = ""
63
+ VERSION: ClassVar[int] = 2
64
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, SystemSubject.CATALOG))
64
65
  OBJ_REF: ClassVar[str] = "subscriptionplan"
65
66
 
66
- # System-managed fields
67
- schema_version: float = Field(
68
- default=VERSION,
69
- description="Version of this Class == version of DB Schema",
70
- frozen=True
71
- )
72
-
73
67
  id: Optional[str] = Field(
74
68
  default=None,
75
69
  description="Unique identifier for this plan template (e.g., 'free_subscription_1'). Auto-generated if not provided.",
@@ -9,9 +9,9 @@ based on their user type (superadmin, admin, internal, authenticated, anonymous)
9
9
  from typing import Dict, Any, Optional, ClassVar, List
10
10
  from datetime import datetime
11
11
  from pydantic import Field, ConfigDict, field_validator, model_validator
12
- from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, ObjectOverallStatus
12
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, SystemSubject, ObjectOverallStatus
13
13
  from ipulse_shared_base_ftredge.enums.enums_iam import IAMUserType
14
- from ipulse_shared_core_ftredge.models.base_data_model import BaseDataModel
14
+ from ipulse_shared_core_ftredge.models.base_nosql_model import BaseNoSQLModel
15
15
  from ipulse_shared_core_ftredge.models.user.user_permissions import UserPermission
16
16
 
17
17
  # ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
@@ -19,7 +19,7 @@ from ipulse_shared_core_ftredge.models.user.user_permissions import UserPermissi
19
19
 
20
20
 
21
21
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION IF SCHEMA IS BEING MODIFIED !!! ############################################
22
- class UserType(BaseDataModel):
22
+ class UserType(BaseNoSQLModel):
23
23
  """
24
24
  Configuration template for user type defaults stored in Firestore.
25
25
  These templates define the default settings applied when creating users of specific types.
@@ -27,17 +27,12 @@ class UserType(BaseDataModel):
27
27
 
28
28
  model_config = ConfigDict(extra="forbid")
29
29
 
30
- VERSION: ClassVar[float] = 1.0
31
- DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.CATALOG.name))
30
+ SCHEMA_ID: ClassVar[str] = ""
31
+ SCHEMA_NAME: ClassVar[str] = ""
32
+ VERSION: ClassVar[int] = 1
33
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, SystemSubject.CATALOG.name))
32
34
  OBJ_REF: ClassVar[str] = "usertype"
33
35
 
34
- # System-managed fields
35
- schema_version: float = Field(
36
- default=VERSION,
37
- description="Version of this Class == version of DB Schema",
38
- frozen=True
39
- )
40
-
41
36
  id: Optional[str] = Field(
42
37
  default=None,
43
38
  description="Unique identifier for this user type template (e.g., 'superadmin_1', 'authenticated_1'). Auto-generated if not provided.",
@@ -0,0 +1,44 @@
1
+ # pylint: disable=missing-module-docstring, missing-class-docstring
2
+ from typing import List, Optional, TypeVar, Generic, ClassVar
3
+ from datetime import datetime
4
+ from pydantic import Field, BaseModel
5
+ from ipulse_shared_core_ftredge.models.base_nosql_model import BaseNoSQLModel
6
+
7
+ # Generic type for the records within the dataset
8
+ RecordsSamplingType = TypeVar('RecordsSamplingType', bound=BaseModel)
9
+
10
+ class TimeSeriesPackagedDatasetModel(BaseNoSQLModel, Generic[RecordsSamplingType]):
11
+ """
12
+ An intermediary model for time series datasets that holds aggregated records.
13
+ It provides a generic way to handle different types of time series records.
14
+ """
15
+ SCHEMA_ID: ClassVar[str] = ""
16
+ SCHEMA_NAME: ClassVar[str] = ""
17
+ VERSION: ClassVar[int] = 1
18
+
19
+
20
+ subject_id: str = Field(default="", description="The unique identifier for the subject.")
21
+ subject_category: str = Field(default="", description="The subject category eg. EQUITY, DERIVATIVE, CRYPTO etc.")
22
+
23
+ # Generic lists for different temporal buckets of records
24
+ max_bulk_records: List[RecordsSamplingType] = Field(default_factory=list)
25
+ latest_bulk_records: Optional[List[RecordsSamplingType]] = Field(default_factory=list)
26
+ latest_intraday_records: Optional[List[RecordsSamplingType]] = Field(default_factory=list)
27
+
28
+ # Metadata fields
29
+ max_bulk_updated_at: Optional[datetime] = None
30
+ max_bulk_updated_by: Optional[str] = None
31
+ max_bulk_recent_date_id: Optional[datetime] = None
32
+ max_bulk_oldest_date_id: Optional[datetime] = None
33
+ latest_bulk_recent_date_id: Optional[datetime] = None
34
+ latest_bulk_oldest_date_id: Optional[datetime] = None
35
+ latest_record_updated_at: Optional[datetime] = None
36
+ latest_record_updated_by: Optional[str] = None
37
+ latest_record_change_id: Optional[str] = None
38
+ latest_intraday_bulk_updated_at: Optional[datetime] = None
39
+ latest_intraday_bulk_updated_by: Optional[str] = None
40
+
41
+ @property
42
+ def id(self) -> str:
43
+ """Return subject_id for backward compatibility and consistency."""
44
+ return self.subject_id
@@ -3,8 +3,8 @@ from dateutil.relativedelta import relativedelta
3
3
  import uuid
4
4
  from typing import Optional, ClassVar, Dict, Any, List
5
5
  from pydantic import Field, ConfigDict, model_validator
6
- from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, SubscriptionPlanName, SubscriptionStatus, TimeUnit
7
- from ..base_data_model import BaseDataModel
6
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, SystemSubject, SubscriptionPlanName, SubscriptionStatus, TimeUnit
7
+ from ..base_nosql_model import BaseNoSQLModel
8
8
  from .user_permissions import UserPermission
9
9
  # ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
10
10
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
@@ -14,23 +14,19 @@ DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlanName.FREE_SUBSCRIPTION
14
14
  DEFAULT_SUBSCRIPTION_STATUS = SubscriptionStatus.ACTIVE
15
15
 
16
16
  ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
17
- class UserSubscription(BaseDataModel):
17
+ class UserSubscription(BaseNoSQLModel):
18
18
  """
19
19
  Represents a single subscription cycle with enhanced flexibility and tracking.
20
20
  """
21
21
 
22
22
  model_config = ConfigDict(frozen=True, extra="forbid")
23
23
 
24
- VERSION: ClassVar[float] = 3.0 # Incremented version for direct fields instead of computed
25
- DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.SUBSCRIPTION))
24
+ SCHEMA_ID: ClassVar[str] = ""
25
+ SCHEMA_NAME: ClassVar[str] = ""
26
+ VERSION: ClassVar[int] = 3 # Incremented version for direct fields instead of computed
27
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, SystemSubject.SUBSCRIPTION))
26
28
  OBJ_REF: ClassVar[str] = "subscription"
27
29
 
28
- # System-managed fields (read-only)
29
- schema_version: float = Field(
30
- default=VERSION,
31
- description="Version of this Class == version of DB Schema",
32
- frozen=True
33
- )
34
30
 
35
31
  # Unique identifier for this specific subscription instance - now auto-generated
36
32
  id: Optional[str] = Field(
@@ -3,28 +3,25 @@ from datetime import date, datetime
3
3
  import re # Add re import
4
4
  from typing import Set, Optional, ClassVar, Dict, Any, List
5
5
  from pydantic import EmailStr, Field, ConfigDict, model_validator, field_validator
6
- from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, IAMUserType
7
- from ..base_data_model import BaseDataModel
6
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, SystemSubject, IAMUserType
7
+ from ..base_nosql_model import BaseNoSQLModel
8
8
  # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
9
9
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
10
10
 
11
11
  ############################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! #################################
12
- class UserProfile(BaseDataModel):
12
+ class UserProfile(BaseNoSQLModel):
13
13
  """
14
14
  User Profile model for storing personal information and settings.
15
15
  """
16
16
  model_config = ConfigDict(frozen=False, extra="forbid") # Allow field modification
17
17
 
18
18
  # Class constants
19
- VERSION: ClassVar[float] = 5.0 # Incremented version for primary_usertype addition
20
- DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.USER))
19
+ SCHEMA_ID: ClassVar[str] = ""
20
+ SCHEMA_NAME: ClassVar[str] = ""
21
+ VERSION: ClassVar[int] = 5 # Incremented version for primary_usertype addition
22
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, SystemSubject.USER))
21
23
  OBJ_REF: ClassVar[str] = "userprofile"
22
24
 
23
- schema_version: float = Field(
24
- default=VERSION,
25
- frozen=True,
26
- description="Version of this Class == version of DB Schema"
27
- )
28
25
 
29
26
  id: Optional[str] = Field(
30
27
  default=None, # Will be auto-generated from user_uid if not provided
@@ -104,7 +101,7 @@ class UserProfile(BaseDataModel):
104
101
  description="Additional metadata for the user"
105
102
  )
106
103
 
107
- # Remove audit fields as they're inherited from BaseDataModel
104
+ # Remove audit fields as they're inherited from BaseNoSQLModel
108
105
 
109
106
  @field_validator('user_uid')
110
107
  @classmethod
@@ -2,16 +2,16 @@
2
2
  from datetime import datetime, timezone, timedelta
3
3
  from typing import Set, Optional, Dict, List, ClassVar, Any
4
4
  from pydantic import Field, ConfigDict, model_validator, field_validator
5
- from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, TimeUnit
5
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, SystemSubject, TimeUnit
6
6
  from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnit
7
7
  from .user_subscription import UserSubscription
8
- from ..base_data_model import BaseDataModel
8
+ from ..base_nosql_model import BaseNoSQLModel
9
9
  from .user_permissions import UserPermission
10
10
 
11
11
 
12
12
 
13
13
  ############################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! #################################
14
- class UserStatus(BaseDataModel):
14
+ class UserStatus(BaseNoSQLModel):
15
15
  """
16
16
  User Status model for tracking user subscription and access rights.
17
17
  """
@@ -19,21 +19,14 @@ class UserStatus(BaseDataModel):
19
19
  model_config = ConfigDict(frozen=False, extra="forbid")
20
20
 
21
21
  # Class constants
22
- VERSION: ClassVar[float] = 7.0 # Major version bump for flattened IAM permissions structure
23
- DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.USER))
22
+ SCHEMA_ID: ClassVar[str] = ""
23
+ SCHEMA_NAME: ClassVar[str] = ""
24
+ VERSION: ClassVar[int] = 7 # Major version bump for flattened IAM permissions structure
25
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, SystemSubject.USER))
24
26
  OBJ_REF: ClassVar[str] = "userstatus"
25
-
26
- # Centralized collection name and document ID prefix
27
27
  COLLECTION_NAME: ClassVar[str] = "papp_core_user_userstatuss"
28
28
 
29
29
 
30
- # System-managed fields
31
- schema_version: float = Field(
32
- default=VERSION,
33
- frozen=True,
34
- description="Version of this Class == version of DB Schema"
35
- )
36
-
37
30
  id: Optional[str] = Field(
38
31
  default=None, # Will be auto-generated from user_uid if not provided
39
32
  description=f"User ID, format: {OBJ_REF}_user_uid"
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
  # Import from base services
5
- from .base import BaseFirestoreService, CacheAwareFirestoreService
5
+ from .base import BaseFirestoreService, CacheAwareFirestoreService, MultiCollectionCacheAwareFirestoreService
6
6
 
7
7
  from .charging_processors import ChargingProcessor
8
8
  from .user_charging_service import UserChargingService
@@ -7,6 +7,7 @@ preventing circular import dependencies.
7
7
 
8
8
  from .base_firestore_service import BaseFirestoreService
9
9
  from .cache_aware_firestore_service import CacheAwareFirestoreService
10
+ from .multi_collection_cache_aware_firestore_service import MultiCollectionCacheAwareFirestoreService
10
11
 
11
12
  __all__ = [
12
13
  'BaseFirestoreService',
@@ -1,13 +1,13 @@
1
1
  """Cache-aware Firestore service base class."""
2
2
  import time
3
- from typing import TypeVar, Generic, Dict, Any, List, Optional, Union, Type
3
+ from typing import TypeVar, Generic, Dict, Any, List, Optional, Union, Type, Tuple
4
4
  from google.cloud import firestore
5
5
  from . import BaseFirestoreService
6
6
  from ...exceptions import ResourceNotFoundError, ServiceError
7
7
  from ...cache.shared_cache import SharedCache
8
- from ...models import BaseDataModel
8
+ from ...models import BaseNoSQLModel
9
9
 
10
- T = TypeVar('T', bound=BaseDataModel)
10
+ T = TypeVar('T', bound=BaseNoSQLModel)
11
11
 
12
12
  class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
13
13
  """
@@ -58,7 +58,9 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
58
58
  cache_check_time = (time.time() - start_time) * 1000
59
59
 
60
60
  if cached_doc is not None:
61
- self.logger.debug(f"Cache HIT for document {doc_id} in {cache_check_time:.2f}ms")
61
+ # SharedCache.get() already logs cache hit, only log timing if significant
62
+ if cache_check_time > 5.0: # Only log if cache check took >5ms
63
+ self.logger.debug(f"Cache HIT for document {doc_id} in {cache_check_time:.2f}ms")
62
64
  if convert_to_model and self.model_class:
63
65
  return self._convert_to_model(cached_doc, doc_id)
64
66
  else:
@@ -68,7 +70,66 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
68
70
  self.logger.debug(f"Cache MISS for document {doc_id} - checking Firestore")
69
71
 
70
72
  # Fetch from Firestore using parent method
71
- return await super().get_document(doc_id, convert_to_model)
73
+ result = await super().get_document(doc_id, convert_to_model)
74
+
75
+ # Cache the result if we have a cache and got valid data
76
+ if self.document_cache and result is not None:
77
+ if convert_to_model and isinstance(result, BaseNoSQLModel):
78
+ # Cache the model's dict representation
79
+ self._cache_document_data(doc_id, result.model_dump())
80
+ elif isinstance(result, dict):
81
+ # Cache the dict directly
82
+ self._cache_document_data(doc_id, result)
83
+
84
+ return result
85
+
86
+ async def get_document_with_cache_info(self, doc_id: str, convert_to_model: bool = True) -> Tuple[Union[T, Dict[str, Any], None], bool]:
87
+ """
88
+ Get a document with cache hit information.
89
+
90
+ Args:
91
+ doc_id: Document ID to fetch
92
+ convert_to_model: Whether to convert to Pydantic model
93
+
94
+ Returns:
95
+ Tuple of (document, cache_hit) where cache_hit indicates if from cache
96
+
97
+ Raises:
98
+ ResourceNotFoundError: If document doesn't exist
99
+ """
100
+ cache_hit = False
101
+
102
+ # Check cache first
103
+ if self.document_cache:
104
+ cached_doc = self.document_cache.get(doc_id)
105
+ if cached_doc is not None:
106
+ cache_hit = True
107
+ # Note: SharedCache.get() already logs cache hit at DEBUG level
108
+ if convert_to_model and self.model_class:
109
+ return self._convert_to_model(cached_doc, doc_id), cache_hit
110
+ else:
111
+ cached_doc['id'] = doc_id
112
+ return cached_doc, cache_hit
113
+
114
+ # Cache miss - fetch from Firestore
115
+ self.logger.debug(f"Cache MISS for document {doc_id} - checking Firestore")
116
+
117
+ try:
118
+ result = await super().get_document(doc_id, convert_to_model)
119
+
120
+ # Cache the result if we have a cache and got valid data
121
+ if self.document_cache and result is not None:
122
+ if convert_to_model and isinstance(result, BaseNoSQLModel):
123
+ # Cache the model's dict representation
124
+ self._cache_document_data(doc_id, result.model_dump())
125
+ elif isinstance(result, dict):
126
+ # Cache the dict directly
127
+ self._cache_document_data(doc_id, result)
128
+
129
+ return result, cache_hit
130
+
131
+ except ResourceNotFoundError:
132
+ return None, cache_hit
72
133
 
73
134
  async def get_all_documents(self, cache_key: Optional[str] = None, as_models: bool = True) -> Union[List[T], List[Dict[str, Any]]]:
74
135
  """
@@ -151,7 +212,7 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
151
212
  """Helper to cache document data if document_cache is available."""
152
213
  if self.document_cache:
153
214
  self.document_cache.set(doc_id, data)
154
- self.logger.debug(f"Cached item {doc_id} in {self.document_cache.name}")
215
+ # Note: SharedCache.set() already logs at DEBUG level
155
216
 
156
217
  async def create_document(self, doc_id: str, data: Union[T, Dict[str, Any]], creator_uid: str, merge: bool = False) -> Dict[str, Any]:
157
218
  """Create document and invalidate cache."""
@@ -0,0 +1,244 @@
1
+ """
2
+ Generic multi-collection cache-aware Firestore service.
3
+
4
+ This service extends CacheAwareFirestoreService to support dynamic collection operations
5
+ while maintaining all proven infrastructure patterns. It's designed to be generic and
6
+ reusable across different model types.
7
+ """
8
+ from typing import Dict, Any, List, Optional, Union, Type, TypeVar, Generic
9
+ from google.cloud import firestore
10
+ from .cache_aware_firestore_service import CacheAwareFirestoreService
11
+ from ...exceptions import ServiceError, ValidationError, ResourceNotFoundError
12
+ from ...cache.shared_cache import SharedCache
13
+ from ...models import BaseNoSQLModel
14
+ import logging
15
+
16
+ # Generic type for BaseNoSQLModel subclasses
17
+ T = TypeVar('T', bound=BaseNoSQLModel)
18
+
19
+
20
+ class MultiCollectionCacheAwareFirestoreService(CacheAwareFirestoreService[T], Generic[T]):
21
+ """
22
+ Generic multi-collection extension of CacheAwareFirestoreService.
23
+
24
+ This service extends the proven CacheAwareFirestoreService infrastructure to support
25
+ dynamic collection operations based on storage_location_path while maintaining
26
+ all caching, error handling, and CRUD capabilities.
27
+
28
+ This is a generic base class that can be extended for specific model types.
29
+ """
30
+
31
+ def __init__(self,
32
+ db: firestore.Client,
33
+ logger: logging.Logger,
34
+ model_class: Type[T],
35
+ resource_type: str,
36
+ base_collection_name: str,
37
+ timeout: float = 30.0):
38
+
39
+ # Initialize the parent CacheAwareFirestoreService with a base collection
40
+ # We'll override the collection_name dynamically per operation
41
+ super().__init__(
42
+ db=db,
43
+ collection_name=base_collection_name, # Base collection name
44
+ resource_type=resource_type,
45
+ model_class=model_class,
46
+ logger=logger,
47
+ document_cache=None, # We'll manage caches per collection
48
+ collection_cache=None, # We'll manage caches per collection
49
+ timeout=timeout
50
+ )
51
+
52
+ # Cache for per-collection cache instances
53
+ self._collection_caches: Dict[str, Dict[str, SharedCache]] = {}
54
+
55
+ self.logger.info(f"MultiCollectionCacheAwareFirestoreService initialized for {resource_type}")
56
+
57
+ def _get_collection_caches(self, storage_location_path: str) -> Dict[str, SharedCache]:
58
+ """Get or create cache instances for a specific storage location."""
59
+ if storage_location_path not in self._collection_caches:
60
+ # Create collection-specific cache instances
61
+ # No need for safe_name transformation - dots are fine in strings
62
+
63
+ document_cache = SharedCache(
64
+ name=f"MultiColDoc_{storage_location_path}",
65
+ ttl=600.0, # 10 minutes
66
+ enabled=True,
67
+ logger=self.logger
68
+ )
69
+
70
+ collection_cache = SharedCache(
71
+ name=f"MultiColCollection_{storage_location_path}",
72
+ ttl=600.0, # 10 minutes
73
+ enabled=True,
74
+ logger=self.logger
75
+ )
76
+
77
+ self._collection_caches[storage_location_path] = {
78
+ 'document': document_cache,
79
+ 'collection': collection_cache
80
+ }
81
+
82
+ self.logger.info(f"Created cache instances for collection: {storage_location_path}")
83
+
84
+ return self._collection_caches[storage_location_path]
85
+
86
+ def _set_collection_context(self, storage_location_path: str):
87
+ """Set the collection context for the current operation."""
88
+ # Update the collection name for this operation
89
+ self.collection_name = storage_location_path
90
+
91
+ # Update the cache references for this collection
92
+ caches = self._get_collection_caches(storage_location_path)
93
+ self.document_cache = caches['document']
94
+ self.collection_cache = caches['collection']
95
+
96
+ async def get_document_from_collection(self,
97
+ storage_location_path: str,
98
+ doc_id: str,
99
+ convert_to_model: bool = True) -> Union[T, Dict[str, Any], None]:
100
+ """
101
+ Get a document from a specific collection using the cache-aware infrastructure.
102
+ """
103
+ try:
104
+ # Set collection context
105
+ self._set_collection_context(storage_location_path)
106
+
107
+ # Use the parent's cache-aware get_document method
108
+ return await super().get_document(doc_id, convert_to_model)
109
+
110
+ except ResourceNotFoundError:
111
+ self.logger.info(f"Document {doc_id} not found in {storage_location_path}")
112
+ return None
113
+ except Exception as e:
114
+ self.logger.error(f"Error getting document {doc_id} from {storage_location_path}: {str(e)}", exc_info=True)
115
+ raise ServiceError(
116
+ operation=f"getting document from {storage_location_path}",
117
+ error=e,
118
+ resource_type=self.resource_type,
119
+ resource_id=doc_id
120
+ ) from e
121
+
122
+ async def get_all_documents_from_collection(self,
123
+ storage_location_path: str,
124
+ cache_key: Optional[str] = None) -> List[T]:
125
+ """
126
+ Get all documents from a specific collection using cache-aware infrastructure.
127
+ """
128
+ try:
129
+ # Set collection context
130
+ self._set_collection_context(storage_location_path)
131
+
132
+ # Use cache key if not provided
133
+ if not cache_key:
134
+ cache_key = f"all_documents_{storage_location_path}"
135
+
136
+ # Use the parent's cache-aware get_all_documents method
137
+ results = await super().get_all_documents(cache_key=cache_key, as_models=True)
138
+
139
+ # Ensure we return model instances
140
+ model_results: List[T] = []
141
+ for item in results:
142
+ if isinstance(item, BaseNoSQLModel) and self.model_class and isinstance(item, self.model_class):
143
+ model_results.append(item) # type: ignore
144
+ elif isinstance(item, dict) and self.model_class:
145
+ try:
146
+ model_results.append(self.model_class.model_validate(item))
147
+ except Exception as e:
148
+ self.logger.warning(f"Failed to convert dict to model: {e}")
149
+
150
+ return model_results
151
+
152
+ except Exception as e:
153
+ self.logger.error(f"Error getting all documents from {storage_location_path}: {str(e)}", exc_info=True)
154
+ raise ServiceError(
155
+ operation=f"getting all documents from {storage_location_path}",
156
+ error=e,
157
+ resource_type=self.resource_type
158
+ ) from e
159
+
160
+ async def create_document_in_collection(self,
161
+ storage_location_path: str,
162
+ doc_id: str,
163
+ data: Union[T, Dict[str, Any]],
164
+ creator_uid: str,
165
+ merge: bool = False) -> Dict[str, Any]:
166
+ """
167
+ Create a document in a specific collection using cache-aware infrastructure.
168
+ Automatically handles cache invalidation.
169
+ """
170
+ try:
171
+ # Set collection context
172
+ self._set_collection_context(storage_location_path)
173
+
174
+ # Use the parent's cache-aware create_document method
175
+ return await super().create_document(doc_id, data, creator_uid, merge)
176
+
177
+ except Exception as e:
178
+ self.logger.error(f"Error creating document {doc_id} in {storage_location_path}: {str(e)}", exc_info=True)
179
+ raise ServiceError(
180
+ operation=f"creating document in {storage_location_path}",
181
+ error=e,
182
+ resource_type=self.resource_type,
183
+ resource_id=doc_id
184
+ ) from e
185
+
186
+ async def update_document_in_collection(self,
187
+ storage_location_path: str,
188
+ doc_id: str,
189
+ update_data: Dict[str, Any],
190
+ updater_uid: str,
191
+ require_exists: bool = True) -> Dict[str, Any]:
192
+ """
193
+ Update a document in a specific collection using cache-aware infrastructure.
194
+ Automatically handles cache invalidation.
195
+ """
196
+ try:
197
+ # Set collection context
198
+ self._set_collection_context(storage_location_path)
199
+
200
+ # Use the parent's cache-aware update_document method
201
+ return await super().update_document(doc_id, update_data, updater_uid, require_exists)
202
+
203
+ except Exception as e:
204
+ self.logger.error(f"Error updating document {doc_id} in {storage_location_path}: {str(e)}", exc_info=True)
205
+ raise ServiceError(
206
+ operation=f"updating document in {storage_location_path}",
207
+ error=e,
208
+ resource_type=self.resource_type,
209
+ resource_id=doc_id
210
+ ) from e
211
+
212
+ async def delete_document_from_collection(self,
213
+ storage_location_path: str,
214
+ doc_id: str,
215
+ require_exists: bool = True) -> bool:
216
+ """
217
+ Delete a document from a specific collection using cache-aware infrastructure.
218
+ Automatically handles cache invalidation.
219
+ """
220
+ try:
221
+ # Set collection context
222
+ self._set_collection_context(storage_location_path)
223
+
224
+ # Use the parent's cache-aware delete_document method
225
+ return await super().delete_document(doc_id, require_exists)
226
+
227
+ except Exception as e:
228
+ self.logger.error(f"Error deleting document {doc_id} from {storage_location_path}: {str(e)}", exc_info=True)
229
+ raise ServiceError(
230
+ operation=f"deleting document from {storage_location_path}",
231
+ error=e,
232
+ resource_type=self.resource_type,
233
+ resource_id=doc_id
234
+ ) from e
235
+
236
+ def get_cache_stats(self) -> Dict[str, Any]:
237
+ """Get cache statistics for all collections managed by this service."""
238
+ stats = {}
239
+ for storage_path, caches in self._collection_caches.items():
240
+ stats[storage_path] = {
241
+ 'document_cache': caches['document'].get_stats(),
242
+ 'collection_cache': caches['collection'].get_stats()
243
+ }
244
+ return stats
@@ -358,6 +358,17 @@ class CatalogUserTypeService(BaseFirestoreService[UserType]):
358
358
  limit=1
359
359
  )
360
360
 
361
+ # Fallback to AUTHENTICATED if primary usertype not found (especially for CUSTOMER)
362
+ if not usertypes and primary_usertype == IAMUserType.CUSTOMER:
363
+ self.logger.warning(f"No CUSTOMER usertype found, falling back to AUTHENTICATED for email: {email}")
364
+ usertypes = await self.list_usertypes(
365
+ primary_usertype=IAMUserType.AUTHENTICATED,
366
+ pulse_status=ObjectOverallStatus.ACTIVE,
367
+ latest_version_only=True,
368
+ limit=1
369
+ )
370
+ primary_usertype = IAMUserType.AUTHENTICATED # Update for logging
371
+
361
372
  if not usertypes:
362
373
  from ipulse_shared_core_ftredge.exceptions import ServiceError
363
374
  raise ServiceError(
@@ -56,7 +56,7 @@ class ChargingProcessor:
56
56
  updated_user_credits = pre_fetched_credits
57
57
  elif self.user_charging_service: # Attempt to get current credits if not pre-fetched
58
58
  try:
59
- _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
59
+ _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid=user_uid, required_credits_for_resource=0, pre_fetched_user_credits=None)
60
60
  updated_user_credits = current_user_credits_from_verify
61
61
  except Exception: # pylint: disable=broad-except
62
62
  self.logger.warning(f"Could not fetch current credits for user {user_uid} for free item.")
@@ -77,7 +77,7 @@ class ChargingProcessor:
77
77
  updated_user_credits = pre_fetched_credits
78
78
  elif self.user_charging_service:
79
79
  try:
80
- _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
80
+ _, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid=user_uid, required_credits_for_resource=0, pre_fetched_user_credits=None)
81
81
  updated_user_credits = current_user_credits_from_verify
82
82
  except Exception: # pylint: disable=broad-except
83
83
  self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 27.1.1
3
+ Version: 27.6.2
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
@@ -12,7 +12,7 @@ License-File: LICENCE
12
12
  Requires-Dist: pydantic[email]~=2.5
13
13
  Requires-Dist: python-dateutil~=2.8
14
14
  Requires-Dist: fastapi~=0.115.8
15
- Requires-Dist: ipulse_shared_base_ftredge==11.1.1
15
+ Requires-Dist: ipulse_shared_base_ftredge~=12.6.0
16
16
  Dynamic: author
17
17
  Dynamic: classifier
18
18
  Dynamic: home-page
@@ -21,9 +21,10 @@ src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py
21
21
  src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py
22
22
  src/ipulse_shared_core_ftredge/models/__init__.py
23
23
  src/ipulse_shared_core_ftredge/models/base_api_response.py
24
- src/ipulse_shared_core_ftredge/models/base_data_model.py
24
+ src/ipulse_shared_core_ftredge/models/base_nosql_model.py
25
25
  src/ipulse_shared_core_ftredge/models/credit_api_response.py
26
26
  src/ipulse_shared_core_ftredge/models/custom_json_response.py
27
+ src/ipulse_shared_core_ftredge/models/time_series_packaged_dataset_model.py
27
28
  src/ipulse_shared_core_ftredge/models/catalog/__init__.py
28
29
  src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py
29
30
  src/ipulse_shared_core_ftredge/models/catalog/usertype.py
@@ -41,6 +42,7 @@ src/ipulse_shared_core_ftredge/services/user_charging_service.py
41
42
  src/ipulse_shared_core_ftredge/services/base/__init__.py
42
43
  src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py
43
44
  src/ipulse_shared_core_ftredge/services/base/cache_aware_firestore_service.py
45
+ src/ipulse_shared_core_ftredge/services/base/multi_collection_cache_aware_firestore_service.py
44
46
  src/ipulse_shared_core_ftredge/services/catalog/__init__.py
45
47
  src/ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py
46
48
  src/ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py
@@ -54,7 +56,5 @@ src/ipulse_shared_core_ftredge/services/user/userauth_operations.py
54
56
  src/ipulse_shared_core_ftredge/services/user/userprofile_operations.py
55
57
  src/ipulse_shared_core_ftredge/services/user/userstatus_operations.py
56
58
  src/ipulse_shared_core_ftredge/utils/__init__.py
57
- src/ipulse_shared_core_ftredge/utils/authz_credit_extraction.py
58
59
  src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py
59
- src/ipulse_shared_core_ftredge/utils/json_encoder.py
60
- tests/test_shared_cache.py
60
+ src/ipulse_shared_core_ftredge/utils/json_encoder.py
@@ -1,4 +1,4 @@
1
1
  pydantic[email]~=2.5
2
2
  python-dateutil~=2.8
3
3
  fastapi~=0.115.8
4
- ipulse_shared_base_ftredge==11.1.1
4
+ ipulse_shared_base_ftredge~=12.6.0
@@ -1,147 +0,0 @@
1
- """Tests for the SharedCache implementation."""
2
-
3
- import time
4
- import unittest
5
- import logging
6
- from ipulse_shared_core_ftredge.cache.shared_cache import SharedCache
7
-
8
- # Configure logging for tests
9
- logging.basicConfig(level=logging.INFO)
10
- logger = logging.getLogger(__name__)
11
-
12
-
13
- class TestSharedCache(unittest.TestCase):
14
- """Test cases for SharedCache."""
15
-
16
- def setUp(self):
17
- """Set up test fixtures."""
18
- self.cache = SharedCache[str](
19
- name="TestCache",
20
- ttl=0.5, # Short TTL for faster testing
21
- enabled=True,
22
- logger=logger
23
- )
24
-
25
- def test_cache_set_get(self):
26
- """Test basic cache set and get operations."""
27
- # Set a value
28
- self.cache.set("test_key", "test_value")
29
-
30
- # Get the value
31
- cached_value = self.cache.get("test_key")
32
-
33
- # Verify value was cached
34
- self.assertEqual(cached_value, "test_value")
35
-
36
- def test_cache_ttl_expiration(self):
37
- """Test cache TTL expiration."""
38
- # Set a value
39
- self.cache.set("expiring_key", "expiring_value")
40
-
41
- # Verify it's initially cached
42
- self.assertEqual(self.cache.get("expiring_key"), "expiring_value")
43
-
44
- # Wait for TTL to expire
45
- time.sleep(0.6) # Slightly longer than TTL (0.5s)
46
-
47
- # Verify value is no longer cached
48
- self.assertIsNone(self.cache.get("expiring_key"))
49
-
50
- def test_cache_invalidate(self):
51
- """Test cache invalidation."""
52
- # Set multiple values
53
- self.cache.set("key1", "value1")
54
- self.cache.set("key2", "value2")
55
-
56
- # Invalidate specific key
57
- self.cache.invalidate("key1")
58
-
59
- # Verify key1 is gone but key2 remains
60
- self.assertIsNone(self.cache.get("key1"))
61
- self.assertEqual(self.cache.get("key2"), "value2")
62
-
63
- def test_cache_invalidate_all(self):
64
- """Test invalidating all cache entries."""
65
- # Set multiple values
66
- self.cache.set("key1", "value1")
67
- self.cache.set("key2", "value2")
68
-
69
- # Invalidate all
70
- self.cache.invalidate_all()
71
-
72
- # Verify both keys are gone
73
- self.assertIsNone(self.cache.get("key1"))
74
- self.assertIsNone(self.cache.get("key2"))
75
-
76
- def test_cache_get_or_set(self):
77
- """Test get_or_set functionality."""
78
- # Define a counter to verify how many times the loader is called
79
- counter = [0]
80
-
81
- def data_loader():
82
- counter[0] += 1
83
- return f"loaded_value_{counter[0]}"
84
-
85
- # First call should use data_loader
86
- value1, was_cached1 = self.cache.get_or_set("loader_key", data_loader)
87
-
88
- # Second call should use cached value
89
- value2, was_cached2 = self.cache.get_or_set("loader_key", data_loader)
90
-
91
- # Verify results
92
- self.assertEqual(value1, "loaded_value_1")
93
- self.assertEqual(value2, "loaded_value_1") # Same value from cache
94
- self.assertFalse(was_cached1) # First call was not cached
95
- self.assertTrue(was_cached2) # Second call was cached
96
- self.assertEqual(counter[0], 1) # Loader called exactly once
97
-
98
- def test_cache_disabled(self):
99
- """Test cache behavior when disabled."""
100
- # Create disabled cache
101
- disabled_cache = SharedCache[str](
102
- name="DisabledCache",
103
- ttl=1.0,
104
- enabled=False,
105
- logger=logger
106
- )
107
-
108
- # Set a value
109
- disabled_cache.set("disabled_key", "disabled_value")
110
-
111
- # Attempt to get - should return None since cache is disabled
112
- cached_value = disabled_cache.get("disabled_key")
113
- self.assertIsNone(cached_value)
114
-
115
- def test_cache_generic_typing(self):
116
- """Test cache with different data types."""
117
- # Integer cache
118
- int_cache = SharedCache[int](name="IntCache", ttl=1.0, enabled=True)
119
- int_cache.set("int_key", 123)
120
- self.assertEqual(int_cache.get("int_key"), 123)
121
-
122
- # Dictionary cache
123
- dict_cache = SharedCache[dict](name="DictCache", ttl=1.0, enabled=True)
124
- dict_cache.set("dict_key", {"a": 1, "b": 2})
125
- self.assertEqual(dict_cache.get("dict_key"), {"a": 1, "b": 2})
126
-
127
- def test_cache_stats(self):
128
- """Test cache statistics."""
129
- # Add some data
130
- self.cache.set("stats_key1", "stats_value1")
131
- self.cache.set("stats_key2", "stats_value2")
132
-
133
- # Get stats
134
- stats = self.cache.get_stats()
135
-
136
- # Verify stats
137
- self.assertEqual(stats["name"], "TestCache")
138
- self.assertEqual(stats["enabled"], True)
139
- self.assertEqual(stats["ttl_seconds"], 0.5)
140
- self.assertEqual(stats["item_count"], 2)
141
- self.assertIn("stats_key1", stats["first_20_keys"])
142
- self.assertIn("stats_key2", stats["first_20_keys"])
143
- self.assertEqual(stats["total_keys"], 2)
144
-
145
-
146
- if __name__ == "__main__":
147
- unittest.main()