query-cache-common 1.2.0__tar.gz → 1.3.0__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 (22) hide show
  1. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/.gitignore +3 -0
  2. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/PKG-INFO +2 -1
  3. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/pyproject.toml +1 -0
  4. query_cache_common-1.3.0/src/query_cache_common/auth.py +106 -0
  5. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/explain_service_models.py +4 -0
  6. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/utils.py +15 -0
  7. query_cache_common-1.2.0/src/query_cache_common/auth.py +0 -73
  8. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/__init__.py +0 -0
  9. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/constants.py +0 -0
  10. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/decorators.py +0 -0
  11. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/__init__.py +0 -0
  12. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/_typing.py +0 -0
  13. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/base.py +0 -0
  14. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/converters.py +0 -0
  15. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/fields.py +0 -0
  16. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/__init__.py +0 -0
  17. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/client_telemetry_service_models.py +0 -0
  18. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/client_validation_service_models.py +0 -0
  19. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/clone_service_models.py +0 -0
  20. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/execution_service_models.py +0 -0
  21. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/sql_service_models.py +0 -0
  22. {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/shared_models.py +0 -0
@@ -226,3 +226,6 @@ tests/integration/dbt_run_cache_turbo/corpus_catalog.json
226
226
 
227
227
  # Superpowers (AI agent plans/specs)
228
228
  docs/superpowers/
229
+
230
+ # Git worktrees
231
+ .worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: query-cache-common
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Common code for Query Cache shared across client and server
5
5
  License: Copyright (c) 2026 Fivetran, Inc.
6
6
 
@@ -32,5 +32,6 @@ License: Copyright (c) 2026 Fivetran, Inc.
32
32
  FROM USE OF THIS SOFTWARE, INCLUDING DIRECT, INDIRECT, INCIDENTAL,
33
33
  PUNITIVE, AND CONSEQUENTIAL DAMAGES.
34
34
  Requires-Python: >=3.9
35
+ Requires-Dist: humanize
35
36
  Requires-Dist: query-cache-protobuf
36
37
  Requires-Dist: typing-extensions>=4.0.0
@@ -7,6 +7,7 @@ requires-python = ">=3.9"
7
7
  dependencies = [
8
8
  "typing-extensions>=4.0.0",
9
9
  "query-cache-protobuf",
10
+ "humanize",
10
11
  ]
11
12
 
12
13
  [tool.hatch.version]
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from functools import cached_property
5
+
6
+
7
+ RUNCACHE_ORG_SCOPE_PREFIX = "runcache:scope:org"
8
+ LEGACY_ORG_SCOPE_PREFIX = "conway:scope:org"
9
+ ORG_SCOPE_PREFIXES = (RUNCACHE_ORG_SCOPE_PREFIX, LEGACY_ORG_SCOPE_PREFIX)
10
+ RUNCACHE_APP_SCOPE_PREFIX = "runcache:scope:app"
11
+
12
+
13
+ def _extract_org_id_and_level(scope: str) -> t.Optional[t.Tuple[str, str]]:
14
+ # scope looks something like 'runcache:scope:app:tues_test:developer'
15
+ # that is, SCOPE_PREFIX:{org_id}:{level}
16
+ scope_parts = scope.split(":")
17
+ org_id = scope_parts[3]
18
+ return (org_id, scope_parts[4]) if org_id else None
19
+
20
+
21
+ def _org_id_to_level(scopes: t.List[str]) -> t.Dict[str, str]:
22
+ mapping = filter(None, [_extract_org_id_and_level(scope) for scope in scopes])
23
+ return {org_id: level for org_id, level in mapping}
24
+
25
+
26
+ class Scope:
27
+ def __init__(self, org_scopes: t.List[str], app_scopes: t.List[str]) -> None:
28
+ self._org_scopes = org_scopes
29
+ self._app_scopes = app_scopes
30
+
31
+ for scope in org_scopes + app_scopes:
32
+ if scope.count(":") < 4:
33
+ raise ValueError(f"Invalid scope format: '{scope}'.")
34
+
35
+ @classmethod
36
+ def from_string(cls, scope: str) -> Scope:
37
+ """Create a Scope instance from a scope string.
38
+
39
+ Supports both the current ``runcache:`` prefix and the legacy ``conway:`` prefix.
40
+
41
+ Args:
42
+ scope: The scope string to parse.
43
+
44
+ Returns:
45
+ A Scope instance.
46
+ """
47
+ scopes = [s.strip() for s in scope.split(" ")]
48
+
49
+ org_scopes = [s for s in scopes if any(s.startswith(p) for p in ORG_SCOPE_PREFIXES)]
50
+ app_scopes = [s for s in scopes if s.startswith(RUNCACHE_APP_SCOPE_PREFIX)]
51
+
52
+ return cls(org_scopes=org_scopes, app_scopes=app_scopes)
53
+
54
+ @property
55
+ def org_id(self) -> str:
56
+ """Extract the organization ID from a given scope string.
57
+
58
+ This method raises ValueError if the scope is not associated with exactly one organization ID.
59
+
60
+ Returns:
61
+ The extracted organization ID.
62
+ """
63
+ org_ids = self.org_ids
64
+ if not org_ids:
65
+ raise ValueError("No organization scope found.")
66
+ if len(org_ids) > 1:
67
+ raise ValueError(f"Only one organization scope is supported, got multiple: {org_ids}.")
68
+ org_id = org_ids[0]
69
+ if org_id == "*":
70
+ raise ValueError(f"Wildcard organization ID is not allowed.")
71
+ return org_id
72
+
73
+ @cached_property
74
+ def org_ids(self) -> t.List[str]:
75
+ """Returns all organization IDs from the scope, including wildcards."""
76
+ return list(_org_id_to_level(self._org_scopes))
77
+
78
+ def is_org_id_in_scope(self, org_id: str) -> bool:
79
+ """Check if the given organization ID is included in the scope.
80
+
81
+ Args:
82
+ org_id: The organization ID to check.
83
+
84
+ Returns:
85
+ True if the org_id is in the scope or if a wildcard "*" is present; False otherwise.
86
+ """
87
+ return org_id in self.org_ids or "*" in self.org_ids
88
+
89
+ def is_org_id_disabled(self, org_id: str) -> bool:
90
+ """Check if the user's access has been disabled for the given organization.
91
+
92
+ Args:
93
+ org_id: The organization ID to check.
94
+
95
+ Returns:
96
+ True if the user's access to the organization has been disabled,
97
+ False if the user is either not a member of the organization or is still an active member
98
+ """
99
+
100
+ # if we have an :app: scope containing the org id, but not an :org: scope, the organization has been disabled for the user
101
+ org_ids_in_app_scopes = _org_id_to_level(self._app_scopes)
102
+
103
+ if self.is_org_id_in_scope(org_id):
104
+ return False
105
+
106
+ return org_id in org_ids_in_app_scopes
@@ -57,3 +57,7 @@ class GetExplainMessagesRequest(BaseSerDeModel):
57
57
  @proto_dataclass(explain_service_pb2.GetExplainMessagesResponse)
58
58
  class GetExplainMessagesResponse(BaseSerDeModel):
59
59
  messages: t.List[ExplainMessageEntry]
60
+
61
+ @property
62
+ def by_id(self) -> t.Dict[str, ExplainMessageEntry]:
63
+ return {m.execution_decision_id: m for m in self.messages}
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime
4
+ import humanize
3
5
  import time
4
6
  import typing as t
5
7
 
@@ -114,3 +116,16 @@ def extract_fqn_parts(fqn: str, dialect: str) -> t.Tuple[str, str, str]:
114
116
  raise ValueError(f"Expecting SQLGlot expression for name, got: {name}")
115
117
 
116
118
  return catalog.sql(dialect=dialect), schema.sql(dialect=dialect), name.sql(dialect=dialect)
119
+
120
+
121
+ def format_as_localtime(dt: datetime.datetime) -> str:
122
+ local_dt = dt.astimezone() # convert to localtime
123
+ return local_dt.strftime("%Y-%m-%d %H:%M:%S %Z")
124
+
125
+
126
+ def format_epoch_relative(epoch: t.Optional[int]) -> str:
127
+ if epoch is None:
128
+ return "no data"
129
+
130
+ as_dt = datetime.datetime.fromtimestamp(epoch / 1000, tz=datetime.timezone.utc)
131
+ return f"{humanize.naturaltime(as_dt)}, {format_as_localtime(as_dt)}"
@@ -1,73 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from functools import cached_property
4
-
5
-
6
- RUNCACHE_ORG_SCOPE_PREFIX = "runcache:scope:org"
7
- LEGACY_ORG_SCOPE_PREFIX = "conway:scope:org"
8
- ORG_SCOPE_PREFIXES = (RUNCACHE_ORG_SCOPE_PREFIX, LEGACY_ORG_SCOPE_PREFIX)
9
-
10
-
11
- class Scope:
12
- def __init__(self, org_id_to_level: dict[str, str]) -> None:
13
- self._org_id_to_level: dict[str, str] = org_id_to_level
14
-
15
- @classmethod
16
- def from_string(cls, scope: str) -> Scope:
17
- """Create a Scope instance from a scope string.
18
-
19
- Supports both the current ``runcache:`` prefix and the legacy ``conway:`` prefix.
20
-
21
- Args:
22
- scope: The scope string to parse.
23
-
24
- Returns:
25
- A Scope instance.
26
- """
27
- scopes = scope.split(" ")
28
- org_scopes = [s for s in scopes if any(s.strip().startswith(p) for p in ORG_SCOPE_PREFIXES)]
29
-
30
- org_id_to_level = {}
31
- for org_scope in org_scopes:
32
- scope_parts = org_scope.split(":")
33
- if len(scope_parts) < 5:
34
- raise ValueError(f"Invalid scope format: '{scope}'.")
35
- org_id = scope_parts[3]
36
- if org_id:
37
- org_id_to_level[org_id] = scope_parts[4]
38
- return cls(org_id_to_level=org_id_to_level)
39
-
40
- @property
41
- def org_id(self) -> str:
42
- """Extract the organization ID from a given scope string.
43
-
44
- This method raises ValueError if the scope is not associated with exactly one organization ID.
45
-
46
- Returns:
47
- The extracted organization ID.
48
- """
49
- org_ids = self.org_ids
50
- if not org_ids:
51
- raise ValueError("No organization scope found.")
52
- if len(org_ids) > 1:
53
- raise ValueError(f"Only one organization scope is supported, got multiple: {org_ids}.")
54
- org_id = org_ids[0]
55
- if org_id == "*":
56
- raise ValueError(f"Wildcard organization ID is not allowed.")
57
- return org_id
58
-
59
- @cached_property
60
- def org_ids(self) -> list[str]:
61
- """Returns all organization IDs from the scope, including wildcards."""
62
- return list(self._org_id_to_level)
63
-
64
- def is_org_id_in_scope(self, org_id: str) -> bool:
65
- """Check if the given organization ID is included in the scope.
66
-
67
- Args:
68
- org_id: The organization ID to check.
69
-
70
- Returns:
71
- True if the org_id is in the scope or if a wildcard "*" is present; False otherwise.
72
- """
73
- return org_id in self._org_id_to_level or "*" in self._org_id_to_level