ipulse-shared-core-ftredge 9.1.1__tar.gz → 11.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.

Files changed (36) hide show
  1. {ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-11.1.1}/PKG-INFO +1 -1
  2. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/setup.py +1 -1
  3. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
  4. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +56 -34
  5. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +8 -5
  6. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/subscription.py +1 -1
  7. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +5 -5
  8. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_status.py +9 -9
  9. ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge/utils/__init__.py +1 -0
  10. ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge/utils/json_encoder.py +62 -0
  11. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
  12. ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -1
  13. ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -13
  14. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/LICENCE +0 -0
  15. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/README.md +0 -0
  16. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/pyproject.toml +0 -0
  17. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/setup.cfg +0 -0
  18. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  19. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
  20. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  21. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
  22. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +0 -0
  23. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +0 -0
  24. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -0
  25. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
  26. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
  27. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
  28. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -0
  29. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py +0 -0
  30. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
  31. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
  32. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
  33. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +0 -0
  34. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  35. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
  36. {ipulse_shared_core_ftredge-9.1.1 → ipulse_shared_core_ftredge-11.1.1}/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: 9.1.1
3
+ Version: 11.1.1
4
4
  Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
5
5
  Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
6
6
  Author: Russlan Ramdowar
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name='ipulse_shared_core_ftredge',
6
- version='9.1.1',
6
+ version='11.1.1',
7
7
  package_dir={'': 'src'}, # Specify the source directory
8
8
  packages=find_packages(where='src'), # Look for packages in 'src'
9
9
  install_requires=[
@@ -9,4 +9,4 @@ from .models import ( UserAuth, UserProfile,Subscription,
9
9
  from .services import (BaseFirestoreService,BaseServiceException, ResourceNotFoundError, AuthorizationError,
10
10
  ValidationError)
11
11
 
12
- from .utils import (CustomJSONEncoder)
12
+ from .utils import (EnsureJSONEncoderCompatibility)
@@ -2,16 +2,19 @@ import os
2
2
  import logging
3
3
  from typing import Optional, Iterable, Dict, Any, List
4
4
  from datetime import datetime, timedelta, timezone
5
+ import json
5
6
  import httpx
6
7
  from fastapi import HTTPException, Request
7
8
  from google.cloud import firestore
8
9
  from ipulse_shared_core_ftredge.services import ServiceError, AuthorizationError, ResourceNotFoundError
9
10
  from ipulse_shared_core_ftredge.models import UserStatus
11
+ from ipulse_shared_core_ftredge.utils.json_encoder import convert_to_json_serializable
10
12
 
11
13
  # Constants
12
14
  USERS_STATUS_COLLECTION_NAME = UserStatus.get_collection_name()
13
15
  USERS_STATUS_DOC_REF = "userstatus_"
14
16
  CACHE_TTL = 60 # 60 seconds
17
+
15
18
  class UserStatusCache:
16
19
  """Manages user status caching with dynamic invalidation"""
17
20
  def __init__(self):
@@ -175,41 +178,78 @@ async def authorizeAPIRequest(
175
178
  additional_info={"path": str(request.url)}
176
179
  )
177
180
 
178
-
179
181
  # Determine if we need fresh status
180
182
  force_fresh = _should_force_fresh_status(request)
181
183
  userstatus, cache_used = await get_userstatus(user_uid, db, force_fresh=force_fresh)
182
184
 
183
- # Prepare authorization input
184
- auth_input = {
185
+ # Prepare authorization input that matches OPA expectations
186
+ # Extract required values from user status
187
+ primary_usertype = userstatus.get("primary_usertype")
188
+ secondary_usertypes = userstatus.get("secondary_usertypes", [])
189
+
190
+ # Extract IAM domain permissions
191
+ iam_domain_permissions = userstatus.get("iam_domain_permissions", {})
192
+
193
+ # Format the authz_input to match what the OPA policies expect
194
+ authz_input = {
185
195
  "api_url": request.url.path,
186
196
  "requestor": {
187
197
  "uid": user_uid,
188
- "usertypes": request.state.user.get("usertypes"),
189
- "email_verified": request.state.user.get("email_verified"),
190
- "iam_groups": userstatus.get("iam_groups"),
191
- "subscriptions": userstatus.get("subscriptions"),
192
- "sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits"),
193
- "extra_insight_credits": userstatus.get("extra_insight_credits")
198
+ "primary_usertype": primary_usertype,
199
+ "secondary_usertypes": secondary_usertypes,
200
+ "usertypes": [primary_usertype] + secondary_usertypes if primary_usertype else secondary_usertypes,
201
+ "email_verified": request.state.user.get("email_verified", False),
202
+ "iam_domain_permissions": iam_domain_permissions,
203
+ "sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits", 0),
204
+ "extra_insight_credits": userstatus.get("extra_insight_credits", 0)
194
205
  },
195
206
  "method": request.method.lower(),
196
207
  "request_resource_fields": request_resource_fields
197
208
  }
198
209
 
199
- ####!!!!!!!!!! OPA call
210
+ # Convert any non-serializable objects to JSON serializable format
211
+ # Using the unified utility from utils
212
+ json_safe_authz_input = convert_to_json_serializable(authz_input)
213
+
200
214
  # Query OPA
201
215
  opa_url = f"{os.getenv('OPA_SERVER_URL', 'http://localhost:8181')}{os.getenv('OPA_DECISION_PATH', '/v1/data/http/authz/ingress/decision')}"
202
216
  logger.debug(f"Attempting to connect to OPA at: {opa_url}")
203
- logger.debug(f"Authorization input: {auth_input}")
217
+ logger.debug(f"Authorization input: {authz_input}")
218
+
204
219
  async with httpx.AsyncClient() as client:
205
220
  try:
206
221
  response = await client.post(
207
222
  opa_url,
208
- json={"input": auth_input},
223
+ json={"input": json_safe_authz_input},
209
224
  timeout=5.0 # 5 seconds timeout
210
225
  )
211
226
  logger.debug(f"OPA Response Status: {response.status_code}")
212
227
  logger.debug(f"OPA Response Body: {response.text}")
228
+
229
+ if response.status_code != 200:
230
+ logger.error(f"OPA authorization failed: {response.text}")
231
+ raise HTTPException(
232
+ status_code=500,
233
+ detail="Authorization service error"
234
+ )
235
+
236
+ result = response.json()
237
+ logger.debug(f"Parsed OPA response: {result}")
238
+
239
+ if not result.get("result", {}).get("allow", False):
240
+ logger.error(f"Authorization denied: {result}")
241
+ raise AuthorizationError(
242
+ action=f"{request.method} {request.url.path}",
243
+ additional_info={
244
+ "user_uid": user_uid,
245
+ "resource_fields": request_resource_fields,
246
+ "opa_decision": result.get("result", {})
247
+ }
248
+ )
249
+
250
+ # Extract credit check information from the OPA response
251
+ credit_check = result.get("result", {}).get("credit_check", {})
252
+
213
253
  except httpx.RequestError as e:
214
254
  logger.error(f"Failed to connect to OPA: {str(e)}")
215
255
  raise ServiceError(
@@ -221,39 +261,21 @@ async def authorizeAPIRequest(
221
261
  "connection_error": str(e)
222
262
  }
223
263
  ) from e
224
- if response.status_code != 200:
225
- logger.error(f"OPA authorization failed: {response.text}")
226
- raise HTTPException(
227
- status_code=500,
228
- detail="Authorization service error"
229
- )
230
-
231
- result = response.json()
232
- if not result.get("result", {}).get("allow", False):
233
- raise AuthorizationError(
234
- action=f"{request.method} {request.url.path}",
235
- additional_info={
236
- "user_uid": user_uid,
237
- "resource_fields": request_resource_fields
238
- }
239
- )
240
-
241
- # Extract credit check information from the OPA response
242
- credit_check = {}
243
- if "credit_check" in result.get("result", {}):
244
- credit_check = result["result"]["credit_check"]
245
264
 
246
265
  # More descriptive metadata about the data freshness
247
266
  return {
248
267
  "used_cached_status": cache_used,
249
268
  "required_fresh_status": force_fresh,
250
269
  "status_retrieved_at": datetime.now(timezone.utc).isoformat(),
251
- "credit_check": credit_check
270
+ "credit_check": credit_check,
271
+ "allow_all_fields": result.get("result", {}).get("allow_all_fields", False),
272
+ "allowed_fields": result.get("result", {}).get("allowed_fields", [])
252
273
  }
253
274
 
254
275
  except (AuthorizationError, ResourceNotFoundError):
255
276
  raise
256
277
  except Exception as e:
278
+ logger.exception(f"Exception in authorizeAPIRequest: {e}")
257
279
  raise ServiceError(
258
280
  operation="API authorization",
259
281
  error=e,
@@ -3,7 +3,7 @@ import datetime as dt
3
3
  import json
4
4
  from pydantic import BaseModel, ConfigDict
5
5
  from fastapi.responses import JSONResponse
6
- from ipulse_shared_core_ftredge.utils import CustomJSONEncoder
6
+ from ipulse_shared_core_ftredge.utils.json_encoder import EnsureJSONEncoderCompatibility, convert_to_json_serializable
7
7
 
8
8
 
9
9
  T = TypeVar('T')
@@ -27,7 +27,7 @@ class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
27
27
 
28
28
  class CustomJSONResponse(JSONResponse):
29
29
  def render(self, content) -> bytes:
30
- # Handle Pydantic models to exclude computed fields
30
+ # First preprocess content with our utility function
31
31
  if isinstance(content, dict) and "data" in content and hasattr(content["data"], "model_dump"):
32
32
  # If content["data"] is a Pydantic model, use model_dump with exclude_unset=True
33
33
  # and exclude_computed=True to prevent serialization of computed fields
@@ -37,12 +37,15 @@ class CustomJSONResponse(JSONResponse):
37
37
  exclude_computed=True
38
38
  )
39
39
 
40
- # Use the CustomJSONEncoder for serialization
40
+ # Now convert all problematic types to JSON serializable values
41
+ json_safe_content = convert_to_json_serializable(content)
42
+
43
+ # Use the CustomJSONEncoder for additional safety
41
44
  return json.dumps(
42
- content,
45
+ json_safe_content,
43
46
  ensure_ascii=False,
44
47
  allow_nan=False,
45
48
  indent=None,
46
49
  separators=(",", ":"),
47
- default=CustomJSONEncoder().default
50
+ cls=EnsureJSONEncoderCompatibility
48
51
  ).encode("utf-8")
@@ -90,7 +90,7 @@ class Subscription(BaseDataModel):
90
90
  )
91
91
 
92
92
  # IAM permissions structure
93
- iam_domain_permissions: Dict[str, Dict[str, List[str]]] = Field(
93
+ default_iam_domain_permissions: Dict[str, Dict[str, List[str]]] = Field(
94
94
  ..., # Required field, no default
95
95
  description="IAM domain permissions granted by this subscription (domain -> IAM unit type -> list of unit references)"
96
96
  )
@@ -16,7 +16,7 @@ class UserProfile(BaseDataModel):
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_user_type addition
19
+ VERSION: ClassVar[float] = 5.0 # Incremented version for primary_usertype addition
20
20
  DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
21
21
  OBJ_REF: ClassVar[str] = "userprofile"
22
22
 
@@ -36,14 +36,14 @@ class UserProfile(BaseDataModel):
36
36
  description="User UID from Firebase Auth"
37
37
  )
38
38
 
39
- # Added primary_user_type field for main role categorization
40
- primary_user_type: str = Field(
39
+ # Added primary_usertype field for main role categorization
40
+ primary_usertype: str = Field(
41
41
  ...,
42
42
  description="Primary user type (e.g., customer, internal, admin, superadmin)"
43
43
  )
44
44
 
45
- # Renamed user_types to secondary_user_types
46
- secondary_user_types: List[str] = Field(
45
+ # Renamed user_types to secondary_usertypes
46
+ secondary_usertypes: List[str] = Field(
47
47
  default_factory=list,
48
48
  description="List of secondary user types"
49
49
  )
@@ -68,14 +68,14 @@ class UserStatus(BaseDataModel):
68
68
  description="User UID from Firebase Auth"
69
69
  )
70
70
 
71
- # Added primary_user_type field for main role categorization
72
- primary_user_type: str = Field(
71
+ # Added primary_usertype field for main role categorization
72
+ primary_usertype: str = Field(
73
73
  ...,
74
74
  description="Primary user type (e.g., customer, internal, admin, superadmin)"
75
75
  )
76
76
 
77
- # Renamed user_types to secondary_user_types
78
- secondary_user_types: List[str] = Field(
77
+ # Renamed user_types to secondary_usertypes
78
+ secondary_usertypes: List[str] = Field(
79
79
  default_factory=list,
80
80
  description="List of secondary user types/roles"
81
81
  )
@@ -321,7 +321,7 @@ class UserStatus(BaseDataModel):
321
321
  """
322
322
  added_count = 0
323
323
 
324
- for domain, permissions_by_type in subscription.iam_domain_permissions.items():
324
+ for domain, permissions_by_type in subscription.default_iam_domain_permissions.items():
325
325
  for iam_unit_type_str, iam_unit_refs in permissions_by_type.items():
326
326
  # Convert string to enum if needed for internal processing
327
327
  try:
@@ -484,7 +484,7 @@ class UserStatus(BaseDataModel):
484
484
  validity_time_unit=validity_time_unit,
485
485
  auto_renew=plan_data.get("plan_auto_renewal", False),
486
486
  status=SubscriptionStatus.ACTIVE,
487
- iam_domain_permissions=iam_domain_permissions,
487
+ default_iam_domain_permissions=iam_domain_permissions,
488
488
  fallback_plan_id=plan_data.get("fallback_plan_id_if_current_plan_expired"),
489
489
  price_paid_usd=plan_data.get("plan_per_cycle_price_usd") or 0.0,
490
490
  created_by=source,
@@ -501,14 +501,14 @@ class UserStatus(BaseDataModel):
501
501
 
502
502
  @staticmethod
503
503
  def fetch_user_status_defaults(firestore_client,
504
- primary_user_type: str,
504
+ primary_usertype: str,
505
505
  collection: str = "papp_core_configs_user") -> Dict[str, Any]:
506
506
  """
507
507
  Fetch user status defaults from Firestore.
508
508
 
509
509
  Args:
510
510
  firestore_client: Initialized Firestore client
511
- primary_user_type: Primary type of user (customer, internal, admin, etc)
511
+ primary_usertype: Primary type of user (customer, internal, admin, etc)
512
512
  collection: Collection name for user status defaults
513
513
 
514
514
  Returns:
@@ -531,7 +531,7 @@ class UserStatus(BaseDataModel):
531
531
 
532
532
  # Look for defaults with format "{user_type}_defaults_{version}"
533
533
  for key in data.keys():
534
- if key.startswith(f"{primary_user_type}_defaults_"):
534
+ if key.startswith(f"{primary_usertype}_defaults_"):
535
535
  try:
536
536
  version = int(key.split("_")[-1])
537
537
  if version > latest_version:
@@ -0,0 +1 @@
1
+ from .json_encoder import EnsureJSONEncoderCompatibility
@@ -0,0 +1,62 @@
1
+ import json
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from google.cloud.firestore_v1._helpers import DatetimeWithNanoseconds
5
+ from google.api_core import datetime_helpers
6
+
7
+ class EnsureJSONEncoderCompatibility(json.JSONEncoder):
8
+ """Custom JSON encoder that handles Firestore datetime types and other non-serializable objects."""
9
+ def default(self, obj):
10
+ # Handle datetime types
11
+ if isinstance(obj, (datetime, DatetimeWithNanoseconds, datetime_helpers.DatetimeWithNanoseconds)):
12
+ return obj.isoformat()
13
+ # Handle enum types
14
+ elif isinstance(obj, Enum):
15
+ return obj.value
16
+ # Handle pydantic models
17
+ elif hasattr(obj, 'model_dump'):
18
+ return obj.model_dump()
19
+ # Default behavior for other types
20
+ return super().default(obj)
21
+
22
+ def convert_to_json_serializable(obj):
23
+ """
24
+ Recursively convert objects to JSON serializable format.
25
+ Handles datetime objects, Enums, and nested structures.
26
+
27
+ Args:
28
+ obj: Any Python object that might contain non-serializable types
29
+
30
+ Returns:
31
+ The object with all non-serializable types converted to serializable ones
32
+ """
33
+ # Handle None
34
+ if obj is None:
35
+ return None
36
+
37
+ # Handle datetime objects (including Firestore's DatetimeWithNanoseconds)
38
+ if hasattr(obj, 'isoformat'):
39
+ return obj.isoformat()
40
+
41
+ # Handle Enum values
42
+ elif isinstance(obj, Enum):
43
+ return obj.value
44
+
45
+ # Handle dictionaries
46
+ elif isinstance(obj, dict):
47
+ return {key: convert_to_json_serializable(value) for key, value in obj.items()}
48
+
49
+ # Handle lists and tuples
50
+ elif isinstance(obj, (list, tuple)):
51
+ return [convert_to_json_serializable(item) for item in obj]
52
+
53
+ # Handle sets
54
+ elif isinstance(obj, set):
55
+ return [convert_to_json_serializable(item) for item in obj]
56
+
57
+ # Handle Pydantic models and other objects with model_dump method
58
+ elif hasattr(obj, 'model_dump'):
59
+ return convert_to_json_serializable(obj.model_dump())
60
+
61
+ # Return primitive types as-is
62
+ return obj
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 9.1.1
3
+ Version: 11.1.1
4
4
  Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
5
5
  Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
6
6
  Author: Russlan Ramdowar
@@ -1 +0,0 @@
1
- from .json_encoder import CustomJSONEncoder
@@ -1,13 +0,0 @@
1
- import json
2
- from datetime import datetime
3
- from google.cloud.firestore_v1._helpers import DatetimeWithNanoseconds
4
- from google.api_core import datetime_helpers
5
-
6
- class CustomJSONEncoder(json.JSONEncoder):
7
- """Custom JSON encoder that handles Firestore datetime types."""
8
- def default(self, obj):
9
- if isinstance(obj, (datetime, DatetimeWithNanoseconds)):
10
- return obj.isoformat()
11
- if isinstance(obj, datetime_helpers.DatetimeWithNanoseconds):
12
- return obj.isoformat()
13
- return super().default(obj)