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.
- {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
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/setup.py +2 -2
- {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
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -1
- {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
- 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
- {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
- {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
- ipulse_shared_core_ftredge-27.6.2/src/ipulse_shared_core_ftredge/models/time_series_packaged_dataset_model.py +44 -0
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/services/__init__.py +1 -1
- {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
- {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
- ipulse_shared_core_ftredge-27.6.2/src/ipulse_shared_core_ftredge/services/base/multi_collection_cache_aware_firestore_service.py +244 -0
- {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
- {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
- {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
- {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
- {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
- ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/utils/authz_credit_extraction.py +0 -0
- ipulse_shared_core_ftredge-27.1.1/tests/test_shared_cache.py +0 -147
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/README.md +0 -0
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
- {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
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -0
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-27.1.1 → ipulse_shared_core_ftredge-27.6.2}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {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
- {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
- {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
- {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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 .
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
18
|
-
|
|
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
|
-
|
|
13
|
+
SystemSubject, SubscriptionPlanName,ObjectOverallStatus,
|
|
14
14
|
SubscriptionStatus, TimeUnit)
|
|
15
|
-
from ..
|
|
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(
|
|
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
|
-
|
|
63
|
-
|
|
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,
|
|
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.
|
|
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(
|
|
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
|
-
|
|
31
|
-
|
|
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,
|
|
7
|
-
from ..
|
|
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(
|
|
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
|
-
|
|
25
|
-
|
|
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,
|
|
7
|
-
from ..
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
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,
|
|
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 ..
|
|
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(
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
8
|
+
from ...models import BaseNoSQLModel
|
|
9
9
|
|
|
10
|
-
T = TypeVar('T', bound=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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/
|
|
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
|
ipulse_shared_core_ftredge-27.1.1/src/ipulse_shared_core_ftredge/utils/authz_credit_extraction.py
DELETED
|
File without changes
|
|
@@ -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()
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|