sg-query-cache 0.1.0__tar.gz → 0.2.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.
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/PKG-INFO +29 -33
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/README.md +13 -17
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/pyproject.toml +6 -15
- sg_query_cache-0.2.0/src/sg_query_cache/_async_backend.py +181 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_cache.py +375 -275
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_client.py +741 -502
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_local.py +18 -143
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_support.py +74 -27
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_valkey_control.py +152 -32
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_backend.py +27 -2
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_backend_validation.py +6 -6
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_cache.py +635 -724
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_cache_common.py +9 -14
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_cli.py +124 -63
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_client.py +661 -483
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_client_common.py +382 -195
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_config.py +299 -209
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_dependencies.py +135 -92
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_downstream.py +6 -2
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_event_mapping.py +49 -59
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_local.py +10 -5
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_local_store.py +126 -46
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_metrics.py +9 -1
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_model.py +4 -11
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_policy.py +8 -18
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_query.py +12 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_urls.py +2 -9
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_validation.py +3 -2
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_common.py +41 -39
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_control.py +88 -13
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_scripts.py +128 -21
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/daemon_plugin.py +22 -34
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/valkey.py +359 -204
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/valkey_async.py +936 -300
- sg_query_cache-0.1.0/src/sg_query_cache/_async_backend.py +0 -101
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/.gitignore +0 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/LICENSE +0 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/__init__.py +0 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/__main__.py +0 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_codec.py +0 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_keys.py +1 -1
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_proof.py +0 -0
- {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sg-query-cache
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Query caching for Autodesk Flow Production Tracking
|
|
5
5
|
Project-URL: Documentation, https://github.com/dactylo/sg-query-cache/tree/main/docs
|
|
6
6
|
Project-URL: Homepage, https://github.com/dactylo/sg-query-cache
|
|
@@ -27,8 +27,9 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
sg-query-cache reduces repeated Autodesk Flow Production Tracking API reads from Python workers.
|
|
28
28
|
When render jobs, pipeline tools, or services ask the same question many times, the cache answers
|
|
29
29
|
the repeats from memory or from a shared Valkey service instead of sending another request to the
|
|
30
|
-
site. It wraps your existing synchronous or native asynchronous client
|
|
31
|
-
signatures,
|
|
30
|
+
site. It wraps your existing synchronous or native asynchronous client. The methods it wraps keep
|
|
31
|
+
the client's signatures, and you call any other method or attribute on `raw_client`; see
|
|
32
|
+
[which reads are cached](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/consistency.md#which-reads-are-cached).
|
|
32
33
|
|
|
33
34
|
This documentation uses SG for Flow Production Tracking, formerly named Shotgun and ShotGrid.
|
|
34
35
|
|
|
@@ -36,7 +37,7 @@ Caching trades freshness for fewer requests. Every cached answer has a maximum a
|
|
|
36
37
|
default for a single process. Writes made through the wrapper invalidate the cached answers they
|
|
37
38
|
can affect, and a shared deployment also invalidates answers when SG reports a change. Until then,
|
|
38
39
|
a change made elsewhere can be missing from a cached answer. When a read must reflect SG right now,
|
|
39
|
-
bypass the cache for that read. The [consistency guide](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
40
|
+
bypass the cache for that read. The [consistency guide](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/consistency.md) states the guarantee
|
|
40
41
|
in full.
|
|
41
42
|
|
|
42
43
|
## When to use it
|
|
@@ -68,7 +69,7 @@ sg-query-cache requires Python 3.11 or newer and an SG client in the same enviro
|
|
|
68
69
|
|
|
69
70
|
The official package and sg-python-api both provide the `shotgun_api3` import name, so install only
|
|
70
71
|
one of them. The shared and hierarchical layouts also need two Valkey services; see
|
|
71
|
-
[service layout](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
72
|
+
[service layout](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/operations.md#service-layout).
|
|
72
73
|
|
|
73
74
|
From a checkout, install the library into the environment that runs your SG client:
|
|
74
75
|
|
|
@@ -77,8 +78,8 @@ python -m pip install .
|
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
For the shared or hierarchical layout, include the `valkey` extra. It adds the Valkey client that
|
|
80
|
-
the cache and the `sg-query-cache` operator commands use. The extra
|
|
81
|
-
|
|
81
|
+
the cache and the `sg-query-cache` operator commands use. The extra requires valkey-py
|
|
82
|
+
`>=6.1.1,<6.2`:
|
|
82
83
|
|
|
83
84
|
```sh
|
|
84
85
|
python -m pip install '.[valkey]'
|
|
@@ -101,9 +102,6 @@ from sg_query_cache import (
|
|
|
101
102
|
CachedShotgun,
|
|
102
103
|
ClientContext,
|
|
103
104
|
Identity,
|
|
104
|
-
LocalBackend,
|
|
105
|
-
NullMetrics,
|
|
106
|
-
QueryCache,
|
|
107
105
|
)
|
|
108
106
|
|
|
109
107
|
site_url = os.environ['SHOTGUN_URL']
|
|
@@ -112,7 +110,6 @@ raw_sg = Shotgun(site_url, script_name=script_name, api_key=os.environ['SHOTGUN_
|
|
|
112
110
|
|
|
113
111
|
settings = CacheSettings()
|
|
114
112
|
identity = Identity(kind='script', issuer='shotgun-script', namespace=script_name)
|
|
115
|
-
cache = QueryCache(settings, identity, l1=LocalBackend(settings, monotonic, NullMetrics()))
|
|
116
113
|
context = ClientContext(
|
|
117
114
|
site_url=site_url,
|
|
118
115
|
site_namespace=settings.site_namespace,
|
|
@@ -120,22 +117,21 @@ context = ClientContext(
|
|
|
120
117
|
convert_datetimes_to_utc=raw_sg.config.convert_datetimes_to_utc,
|
|
121
118
|
records_per_page=raw_sg.config.records_per_page,
|
|
122
119
|
)
|
|
123
|
-
sg = CachedShotgun(raw_sg, cache, context)
|
|
124
120
|
|
|
125
121
|
|
|
126
|
-
def timed_read(label: str) -> None:
|
|
122
|
+
def timed_read(sg: CachedShotgun, label: str) -> None:
|
|
127
123
|
started = monotonic()
|
|
128
124
|
assets = sg.find('Asset', [['sg_status_list', 'is', 'ip']], ['code'])
|
|
129
125
|
print(f'{label}: {len(assets)} assets in {monotonic() - started:.3f} s')
|
|
130
126
|
|
|
131
127
|
|
|
132
128
|
try:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
with CachedShotgun.open(raw_sg, settings, identity, context) as sg:
|
|
130
|
+
timed_read(sg, 'first read')
|
|
131
|
+
timed_read(sg, 'second read')
|
|
132
|
+
with sg.bypass():
|
|
133
|
+
timed_read(sg, 'bypass')
|
|
137
134
|
finally:
|
|
138
|
-
cache.close()
|
|
139
135
|
raw_sg.close()
|
|
140
136
|
```
|
|
141
137
|
|
|
@@ -155,13 +151,13 @@ local cache.
|
|
|
155
151
|
The example sets up four pieces:
|
|
156
152
|
|
|
157
153
|
- `CacheSettings()` selects the local layout with default limits. Shared layouts load their
|
|
158
|
-
settings from a TOML file; see [configuration](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
154
|
+
settings from a TOML file; see [configuration](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/configuration.md).
|
|
159
155
|
- `Identity` names the SG permission scope whose results may be reused. Here every process running
|
|
160
156
|
this script shares one identity named after the script; give scripts with different permissions
|
|
161
157
|
different identities. Use stable configuration values, never an API key, session token, or
|
|
162
158
|
process ID, because identity values become part of every cache key.
|
|
163
|
-
- `
|
|
164
|
-
|
|
159
|
+
- `CachedShotgun.open()` builds the cache layers selected by the settings and closes them on
|
|
160
|
+
context exit. The SG client remains yours to close, as the `finally` block does here.
|
|
165
161
|
- `ClientContext` records the raw client's response options, such as page size, so results fetched
|
|
166
162
|
with different options are never mixed. Keep it matched to the raw client's configuration.
|
|
167
163
|
|
|
@@ -211,25 +207,25 @@ SG. `bypass()` is an ordinary `with` block, even in async code.
|
|
|
211
207
|
|
|
212
208
|
Use each async cache on one event loop. `bypass()` and other policy scopes apply to the current
|
|
213
209
|
task, and a task created inside a scope inherits it. Before you close caches in a service with
|
|
214
|
-
long-running tasks, read [async shutdown](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
210
|
+
long-running tasks, read [async shutdown](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/operations.md#async-deadlines-and-shutdown).
|
|
215
211
|
|
|
216
212
|
## Documentation
|
|
217
213
|
|
|
218
214
|
| If you want to... | Read |
|
|
219
215
|
| --------------------------------------------------- | ------------------------------------------------------------- |
|
|
220
|
-
| Understand how fresh results are and when to bypass | [Consistency](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
221
|
-
| Choose a layout and write the settings file | [Configuration](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
222
|
-
| Cache reads for human users | [Identities](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
223
|
-
| Change how long results are kept | [Maximum age rules](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
224
|
-
| Run Valkey and the invalidation plugin | [Operations](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
225
|
-
| Flush cached results after an incident | [Recovery](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
226
|
-
| Collect metrics and set alerts | [Metrics](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
227
|
-
| Measure savings for your workload | [Benchmarks](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
228
|
-
| See changes by version | [Changelog](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
216
|
+
| Understand how fresh results are and when to bypass | [Consistency](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/consistency.md) |
|
|
217
|
+
| Choose a layout and write the settings file | [Configuration](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/configuration.md) |
|
|
218
|
+
| Cache reads for human users | [Identities](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/configuration.md#identities-and-trust) |
|
|
219
|
+
| Change how long results are kept | [Maximum age rules](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/configuration.md#maximum-age-rules) |
|
|
220
|
+
| Run Valkey and the invalidation plugin | [Operations](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/operations.md) |
|
|
221
|
+
| Flush cached results after an incident | [Recovery](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/operations.md#flush-or-rotate-cached-results) |
|
|
222
|
+
| Collect metrics and set alerts | [Metrics](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/operations.md#metrics-and-alerts) |
|
|
223
|
+
| Measure savings for your workload | [Benchmarks](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/benchmarks.md) |
|
|
224
|
+
| See changes by version | [Changelog](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/CHANGELOG.md) |
|
|
229
225
|
|
|
230
226
|
## Development
|
|
231
227
|
|
|
232
|
-
See the [development guide](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
228
|
+
See the [development guide](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/docs/development.md) for the required tools, CI, and dependency locks.
|
|
233
229
|
With those tools installed, run:
|
|
234
230
|
|
|
235
231
|
```sh
|
|
@@ -242,6 +238,6 @@ disposable services.
|
|
|
242
238
|
|
|
243
239
|
## License
|
|
244
240
|
|
|
245
|
-
sg-query-cache is available under the [MIT License](https://github.com/dactylo/sg-query-cache/blob/v0.
|
|
241
|
+
sg-query-cache is available under the [MIT License](https://github.com/dactylo/sg-query-cache/blob/v0.2.0/LICENSE). It is an independent project and is
|
|
246
242
|
not affiliated with or endorsed by Autodesk. Flow Production Tracking is a product of Autodesk,
|
|
247
243
|
Inc.
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
sg-query-cache reduces repeated Autodesk Flow Production Tracking API reads from Python workers.
|
|
4
4
|
When render jobs, pipeline tools, or services ask the same question many times, the cache answers
|
|
5
5
|
the repeats from memory or from a shared Valkey service instead of sending another request to the
|
|
6
|
-
site. It wraps your existing synchronous or native asynchronous client
|
|
7
|
-
signatures,
|
|
6
|
+
site. It wraps your existing synchronous or native asynchronous client. The methods it wraps keep
|
|
7
|
+
the client's signatures, and you call any other method or attribute on `raw_client`; see
|
|
8
|
+
[which reads are cached](docs/consistency.md#which-reads-are-cached).
|
|
8
9
|
|
|
9
10
|
This documentation uses SG for Flow Production Tracking, formerly named Shotgun and ShotGrid.
|
|
10
11
|
|
|
@@ -53,8 +54,8 @@ python -m pip install .
|
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
For the shared or hierarchical layout, include the `valkey` extra. It adds the Valkey client that
|
|
56
|
-
the cache and the `sg-query-cache` operator commands use. The extra
|
|
57
|
-
|
|
57
|
+
the cache and the `sg-query-cache` operator commands use. The extra requires valkey-py
|
|
58
|
+
`>=6.1.1,<6.2`:
|
|
58
59
|
|
|
59
60
|
```sh
|
|
60
61
|
python -m pip install '.[valkey]'
|
|
@@ -77,9 +78,6 @@ from sg_query_cache import (
|
|
|
77
78
|
CachedShotgun,
|
|
78
79
|
ClientContext,
|
|
79
80
|
Identity,
|
|
80
|
-
LocalBackend,
|
|
81
|
-
NullMetrics,
|
|
82
|
-
QueryCache,
|
|
83
81
|
)
|
|
84
82
|
|
|
85
83
|
site_url = os.environ['SHOTGUN_URL']
|
|
@@ -88,7 +86,6 @@ raw_sg = Shotgun(site_url, script_name=script_name, api_key=os.environ['SHOTGUN_
|
|
|
88
86
|
|
|
89
87
|
settings = CacheSettings()
|
|
90
88
|
identity = Identity(kind='script', issuer='shotgun-script', namespace=script_name)
|
|
91
|
-
cache = QueryCache(settings, identity, l1=LocalBackend(settings, monotonic, NullMetrics()))
|
|
92
89
|
context = ClientContext(
|
|
93
90
|
site_url=site_url,
|
|
94
91
|
site_namespace=settings.site_namespace,
|
|
@@ -96,22 +93,21 @@ context = ClientContext(
|
|
|
96
93
|
convert_datetimes_to_utc=raw_sg.config.convert_datetimes_to_utc,
|
|
97
94
|
records_per_page=raw_sg.config.records_per_page,
|
|
98
95
|
)
|
|
99
|
-
sg = CachedShotgun(raw_sg, cache, context)
|
|
100
96
|
|
|
101
97
|
|
|
102
|
-
def timed_read(label: str) -> None:
|
|
98
|
+
def timed_read(sg: CachedShotgun, label: str) -> None:
|
|
103
99
|
started = monotonic()
|
|
104
100
|
assets = sg.find('Asset', [['sg_status_list', 'is', 'ip']], ['code'])
|
|
105
101
|
print(f'{label}: {len(assets)} assets in {monotonic() - started:.3f} s')
|
|
106
102
|
|
|
107
103
|
|
|
108
104
|
try:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
with CachedShotgun.open(raw_sg, settings, identity, context) as sg:
|
|
106
|
+
timed_read(sg, 'first read')
|
|
107
|
+
timed_read(sg, 'second read')
|
|
108
|
+
with sg.bypass():
|
|
109
|
+
timed_read(sg, 'bypass')
|
|
113
110
|
finally:
|
|
114
|
-
cache.close()
|
|
115
111
|
raw_sg.close()
|
|
116
112
|
```
|
|
117
113
|
|
|
@@ -136,8 +132,8 @@ The example sets up four pieces:
|
|
|
136
132
|
this script shares one identity named after the script; give scripts with different permissions
|
|
137
133
|
different identities. Use stable configuration values, never an API key, session token, or
|
|
138
134
|
process ID, because identity values become part of every cache key.
|
|
139
|
-
- `
|
|
140
|
-
|
|
135
|
+
- `CachedShotgun.open()` builds the cache layers selected by the settings and closes them on
|
|
136
|
+
context exit. The SG client remains yours to close, as the `finally` block does here.
|
|
141
137
|
- `ClientContext` records the raw client's response options, such as page size, so results fetched
|
|
142
138
|
with different options are never mixed. Keep it matched to the raw client's configuration.
|
|
143
139
|
|
|
@@ -10,7 +10,7 @@ requires = [ "editables~=0.3", "hatch-fancy-pypi-readme>=25.1", "hatchling>=1.27
|
|
|
10
10
|
# ==========================================================
|
|
11
11
|
[project]
|
|
12
12
|
name = "sg-query-cache"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.2.0"
|
|
14
14
|
description = "Query caching for Autodesk Flow Production Tracking"
|
|
15
15
|
keywords = [ "cache", "flow-production-tracking", "queries", "shotgrid", "shotgun", "valkey" ]
|
|
16
16
|
license = "MIT"
|
|
@@ -52,6 +52,7 @@ dev = [
|
|
|
52
52
|
"pyproject-fmt",
|
|
53
53
|
"pyright",
|
|
54
54
|
"ruff",
|
|
55
|
+
"zizmor",
|
|
55
56
|
{ include-group = "test" },
|
|
56
57
|
]
|
|
57
58
|
test = [
|
|
@@ -100,25 +101,13 @@ lint.ignore = [
|
|
|
100
101
|
"Q003", # Conflicts with the Ruff formatter
|
|
101
102
|
"TRY003", # Avoid specifying long messages outside exception class
|
|
102
103
|
]
|
|
103
|
-
# Same-file collaborators use private implementation members.
|
|
104
104
|
lint.per-file-ignores."benchmarks/async_query_cache.py" = [
|
|
105
|
-
"
|
|
106
|
-
"T201", # print() found
|
|
105
|
+
"T201", # print() found
|
|
107
106
|
]
|
|
108
107
|
lint.per-file-ignores."benchmarks/query_cache.py" = [
|
|
109
|
-
"
|
|
110
|
-
"T201", # print() found
|
|
108
|
+
"T201", # print() found
|
|
111
109
|
]
|
|
112
|
-
lint.per-file-ignores."src/sg_query_cache/_async_cache.py" = [ "SLF001" ]
|
|
113
|
-
lint.per-file-ignores."src/sg_query_cache/_async_client.py" = [ "SLF001" ]
|
|
114
|
-
lint.per-file-ignores."src/sg_query_cache/_cache.py" = [ "SLF001" ]
|
|
115
110
|
lint.per-file-ignores."src/sg_query_cache/_cli.py" = [ "T201" ] # print() found
|
|
116
|
-
lint.per-file-ignores."src/sg_query_cache/_dependencies.py" = [ "SLF001" ]
|
|
117
|
-
lint.per-file-ignores."src/sg_query_cache/_event_mapping.py" = [ "SLF001" ]
|
|
118
|
-
lint.per-file-ignores."src/sg_query_cache/_local_store.py" = [ "SLF001" ]
|
|
119
|
-
lint.per-file-ignores."src/sg_query_cache/daemon_plugin.py" = [ "SLF001" ]
|
|
120
|
-
lint.per-file-ignores."src/sg_query_cache/valkey.py" = [ "SLF001" ]
|
|
121
|
-
lint.per-file-ignores."src/sg_query_cache/valkey_async.py" = [ "SLF001" ]
|
|
122
111
|
lint.per-file-ignores."tests/**/*.py" = [
|
|
123
112
|
"D", # No docstrings required in tests
|
|
124
113
|
"PLR2004", # Allow magic values in tests
|
|
@@ -140,6 +129,8 @@ max_supported_python = "3.15"
|
|
|
140
129
|
# ==========================================================
|
|
141
130
|
[tool.pyright]
|
|
142
131
|
pythonVersion = "3.11" # Keep in sync with tool.ruff.target-version
|
|
132
|
+
venv = ".venv" # Created by make setup
|
|
133
|
+
venvPath = "."
|
|
143
134
|
typeCheckingMode = "strict"
|
|
144
135
|
extraPaths = [ ".", "src" ]
|
|
145
136
|
executionEnvironments = [
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Define the backend-neutral asynchronous cache storage contract."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from sg_query_cache._model import (
|
|
6
|
+
CacheHit,
|
|
7
|
+
DependencyTag,
|
|
8
|
+
FillLease,
|
|
9
|
+
GenerationChange,
|
|
10
|
+
GenerationVector,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncCacheBackend(Protocol):
|
|
15
|
+
"""Store opaque encoded results through loop-native coroutine methods.
|
|
16
|
+
|
|
17
|
+
Each method preserves the corresponding `CacheBackend` arguments, return
|
|
18
|
+
values, and backend errors while adding coroutine cancellation cooperation.
|
|
19
|
+
This protocol types the shipped backends; third-party implementations are
|
|
20
|
+
not yet a supported extension point.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
async def change(self, changes: tuple[GenerationChange, ...]) -> None:
|
|
24
|
+
"""Replace all applicable generations in caller order.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
changes: Ordered generation replacements to acknowledge together.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
BackendCapacity: Required generation metadata cannot be retained.
|
|
31
|
+
BackendProtocolError: The ordered change tuple is malformed.
|
|
32
|
+
BackendUnavailable: The change cannot complete reliably.
|
|
33
|
+
StaleIncarnation: The backend incarnation is no longer current.
|
|
34
|
+
RuntimeError: The backend rejects the operation's lifecycle or
|
|
35
|
+
event-loop context.
|
|
36
|
+
CancelledError: The caller cancels the asynchronous operation.
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
async def close(self) -> None:
|
|
41
|
+
"""Release backend resources; repeated same-loop calls have no effect.
|
|
42
|
+
|
|
43
|
+
Backends with active asynchronous work join it before terminal cleanup.
|
|
44
|
+
Caller cancellation may be deferred until owned cleanup settles.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
RuntimeError: The close uses another event loop or would wait on
|
|
48
|
+
its own admitted operation.
|
|
49
|
+
BackendCapacity: Owned cleanup reports a capacity failure.
|
|
50
|
+
BackendProtocolError: Owned cleanup reports malformed client state.
|
|
51
|
+
BackendUnavailable: Owned cleanup cannot complete reliably.
|
|
52
|
+
CancelledError: Caller cancellation propagates after required
|
|
53
|
+
cleanup settlement.
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
async def commit(self, lease: FillLease, payload: bytes, remaining_seconds: float) -> bool:
|
|
58
|
+
"""Store encoded bytes only while the exact lease remains current.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
lease: Exact key and token returned by `reserve`.
|
|
62
|
+
payload: Closed-codec bytes to retain.
|
|
63
|
+
remaining_seconds: Positive authoritative lifetime remaining.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Whether the lease remained current and the result was admitted.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
BackendCapacity: Result-role preparation or oversized-payload lease
|
|
70
|
+
cleanup encounters a capacity failure.
|
|
71
|
+
BackendProtocolError: An argument is outside the backend contract.
|
|
72
|
+
BackendUnavailable: The commit cannot complete reliably.
|
|
73
|
+
StaleIncarnation: The backend incarnation is no longer current.
|
|
74
|
+
RuntimeError: The backend rejects the operation's lifecycle or
|
|
75
|
+
event-loop context.
|
|
76
|
+
CancelledError: The caller cancels the asynchronous operation.
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
async def flush_site(self, site_namespace: str, reason: str) -> None:
|
|
81
|
+
"""Replace one site's permanent site generation.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
site_namespace: Stable configured site namespace.
|
|
85
|
+
reason: Short operator-facing reason code.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
BackendCapacity: Required global metadata cannot be retained.
|
|
89
|
+
BackendProtocolError: The site namespace or reason is malformed.
|
|
90
|
+
BackendUnavailable: The flush cannot complete reliably.
|
|
91
|
+
StaleIncarnation: The backend incarnation is no longer current.
|
|
92
|
+
RuntimeError: The backend rejects the operation's lifecycle or
|
|
93
|
+
event-loop context.
|
|
94
|
+
CancelledError: The caller cancels the asynchronous operation.
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
async def lookup(self, key: str) -> CacheHit | None:
|
|
99
|
+
"""Read valid encoded bytes with backend-authoritative remaining age.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
key: Opaque generation-qualified result key.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
An isolated hit, optionally carrying the backend's fresh decoded
|
|
106
|
+
value, or `None` for absence, expiry, or corruption.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
BackendCapacity: The result role rejects required work for
|
|
110
|
+
capacity.
|
|
111
|
+
BackendProtocolError: The key or backend reply is malformed.
|
|
112
|
+
BackendUnavailable: The lookup cannot complete reliably.
|
|
113
|
+
StaleIncarnation: The backend incarnation is no longer current.
|
|
114
|
+
RuntimeError: The backend rejects the operation's lifecycle or
|
|
115
|
+
event-loop context.
|
|
116
|
+
CancelledError: The caller cancels the asynchronous operation.
|
|
117
|
+
"""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
async def release(self, lease: FillLease) -> bool:
|
|
121
|
+
"""Remove a fill lease only when its exact token remains current.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
lease: Exact key and token returned by `reserve`.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Whether the current lease was removed.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
BackendCapacity: The result role rejects required work for
|
|
131
|
+
capacity.
|
|
132
|
+
BackendProtocolError: The lease or backend reply is malformed.
|
|
133
|
+
BackendUnavailable: The release cannot complete reliably.
|
|
134
|
+
StaleIncarnation: The backend incarnation is no longer current.
|
|
135
|
+
RuntimeError: The backend rejects the operation's lifecycle or
|
|
136
|
+
event-loop context.
|
|
137
|
+
CancelledError: The caller cancels the asynchronous operation.
|
|
138
|
+
"""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
async def reserve(self, key: str, lease_seconds: float) -> FillLease | None:
|
|
142
|
+
"""Acquire temporary fill ownership for one opaque key.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
key: Opaque generation-qualified result key.
|
|
146
|
+
lease_seconds: Positive ownership duration in seconds.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
A fresh lease, or `None` while another lease remains current.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
BackendCapacity: Metadata capacity cannot retain a new lease.
|
|
153
|
+
BackendProtocolError: An argument is outside the backend contract.
|
|
154
|
+
BackendUnavailable: The reservation cannot complete reliably.
|
|
155
|
+
StaleIncarnation: The backend incarnation is no longer current.
|
|
156
|
+
RuntimeError: The backend rejects the operation's lifecycle or
|
|
157
|
+
event-loop context.
|
|
158
|
+
CancelledError: The caller cancels the asynchronous operation.
|
|
159
|
+
"""
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
async def resolve(self, tags: tuple[DependencyTag, ...]) -> GenerationVector:
|
|
163
|
+
"""Resolve one complete bounded dependency vector in request order.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
tags: Ordered unique dependency tags.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Every requested tag and current token under one incarnation.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
BackendCapacity: Metadata capacity cannot retain the vector.
|
|
173
|
+
BackendProtocolError: The vector is malformed or exceeds its
|
|
174
|
+
bounds.
|
|
175
|
+
BackendUnavailable: Resolution cannot complete reliably.
|
|
176
|
+
StaleIncarnation: The backend incarnation is no longer current.
|
|
177
|
+
RuntimeError: The backend rejects the operation's lifecycle or
|
|
178
|
+
event-loop context.
|
|
179
|
+
CancelledError: The caller cancels the asynchronous operation.
|
|
180
|
+
"""
|
|
181
|
+
...
|