Coverage for functions \ flipdare \ util \ debug_util.py: 100%
0 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13import json
14from collections.abc import Mapping
15from typing import Any
17from google.cloud.firestore import Increment
18from google.cloud.firestore_v1.transforms import Sentinel
19from google.cloud.firestore_v1.base_query import FieldFilter, BaseCompositeFilter
21from google.cloud.firestore_v1 import Query, CollectionReference
23__all__ = ["stringify_debug", "stringify_query"]
26# NOTE: -------------------------------------------------------------------
27# NOTE: This should not be returned to the user, only for internal debugging
28# NOTE: -------------------------------------------------------------------
31def stringify_debug(data: dict[str, Any] | Mapping[str, Any]) -> str:
33 def json_fallback(obj: Any) -> str:
34 """Handles Firestore-specific types and any other unknowns."""
35 if isinstance(obj, Sentinel):
36 return "<sentinel-timestamp>"
37 elif isinstance(obj, Increment):
38 return "(Increment)"
39 # Fallback for any other custom objects (like your Models)
40 return str(obj)
42 def transform(item: Any, key_name: str | None = None) -> Any:
43 # 1. Key-based masking
44 if key_name:
45 if key_name.endswith("At"):
46 return "<timestamp>"
47 if key_name.startswith("hash"):
48 return "<hash>"
50 # 2. Handle None explicitly (to get "NONE" instead of null)
51 if item is None:
52 return "NONE"
54 # 3. Recursive traversal
55 if isinstance(item, dict):
56 return {k: transform(v, k) for k, v in item.items()} # type: ignore
57 if isinstance(item, list):
58 return [transform(i) for i in item]
60 return item
62 # special case, one string key, one string value.
63 if isinstance(data, dict) and len(data) == 1:
64 only_key, only_value = next(iter(data.items()))
65 if isinstance(only_value, str):
66 return f"{only_key}={only_value}"
68 # Clean the data keys/None values first
69 clean_data = transform(data)
71 # Dump with the Firestore-type fallback
72 return json.dumps(clean_data, indent=2, default=json_fallback, sort_keys=True)
75def stringify_query(query: Query | CollectionReference) -> str: # pragma: no cover
76 msg = str(query)
77 parent = getattr(query, "_parent", None)
78 if parent:
79 msg = f"{parent.id!s} -> {msg}"
81 filter_parts = []
82 # New-style composite filter (query.where(filter=...)) is stored in _query_filter
83 query_filter = getattr(query, "_query_filter", None)
84 if query_filter:
85 filter_parts.append(_filter_to_string(query_filter))
87 # Old-style field filters (query.where("field", "op", value))
88 field_filters = getattr(query, "_field_filters", [])
89 filter_parts.extend(_filter_to_string(f) for f in field_filters)
91 filter_str = "\t" + " AND ".join(filter_parts) if filter_parts else "No filters"
93 msg += "\n------------------------ Filter -----------------------\n"
94 msg += f"{filter_str}"
95 msg += "\n-------------------------------------------------------\n"
96 return msg
99def _filter_to_string(filter_obj: Any) -> str:
100 if isinstance(filter_obj, FieldFilter):
101 # Access field_path, op_string, and value from the FieldFilter object
102 return f"{filter_obj.field_path} {filter_obj.op_string} {filter_obj.value}"
104 elif isinstance(filter_obj, BaseCompositeFilter):
105 # Recursively handle nested filters in AND/OR blocks
106 op = "AND" if "And" in str(type(filter_obj)) else "OR"
107 parts = [_filter_to_string(f) for f in filter_obj.filters]
108 return f"({f' {op} '.join(parts)})"
110 return str(filter_obj)