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.
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/.gitignore +3 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/PKG-INFO +2 -1
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/pyproject.toml +1 -0
- query_cache_common-1.3.0/src/query_cache_common/auth.py +106 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/explain_service_models.py +4 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/utils.py +15 -0
- query_cache_common-1.2.0/src/query_cache_common/auth.py +0 -73
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/__init__.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/constants.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/decorators.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/__init__.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/_typing.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/base.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/converters.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/fields.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/__init__.py +0 -0
- {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
- {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
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/clone_service_models.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/execution_service_models.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/services/sql_service_models.py +0 -0
- {query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/shared_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: query-cache-common
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/__init__.py
RENAMED
|
File without changes
|
{query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/_typing.py
RENAMED
|
File without changes
|
|
File without changes
|
{query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/converters.py
RENAMED
|
File without changes
|
{query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{query_cache_common-1.2.0 → query_cache_common-1.3.0}/src/query_cache_common/models/shared_models.py
RENAMED
|
File without changes
|