sg-query-cache 0.1.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/.gitignore +46 -0
- sg_query_cache-0.1.0/LICENSE +21 -0
- sg_query_cache-0.1.0/PKG-INFO +247 -0
- sg_query_cache-0.1.0/README.md +223 -0
- sg_query_cache-0.1.0/pyproject.toml +175 -0
- sg_query_cache-0.1.0/src/sg_query_cache/__init__.py +39 -0
- sg_query_cache-0.1.0/src/sg_query_cache/__main__.py +8 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_async_backend.py +101 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_async_cache.py +1240 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_async_client.py +1390 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_async_local.py +318 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_async_support.py +314 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_async_valkey_control.py +580 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_backend.py +168 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_backend_validation.py +110 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_cache.py +1608 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_cache_common.py +387 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_cli.py +313 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_client.py +1061 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_client_common.py +600 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_codec.py +345 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_config.py +1217 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_dependencies.py +888 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_downstream.py +141 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_event_mapping.py +409 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_keys.py +298 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_local.py +301 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_local_store.py +604 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_metrics.py +200 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_model.py +546 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_policy.py +378 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_query.py +53 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_urls.py +324 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_validation.py +70 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_valkey_common.py +1037 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_valkey_control.py +437 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_valkey_proof.py +64 -0
- sg_query_cache-0.1.0/src/sg_query_cache/_valkey_scripts.py +454 -0
- sg_query_cache-0.1.0/src/sg_query_cache/daemon_plugin.py +159 -0
- sg_query_cache-0.1.0/src/sg_query_cache/py.typed +0 -0
- sg_query_cache-0.1.0/src/sg_query_cache/valkey.py +1533 -0
- sg_query_cache-0.1.0/src/sg_query_cache/valkey_async.py +1568 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
.eggs/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
*.egg
|
|
16
|
+
|
|
17
|
+
# Virtual environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
ENV/
|
|
21
|
+
|
|
22
|
+
# PyInstaller
|
|
23
|
+
*.manifest
|
|
24
|
+
*.spec
|
|
25
|
+
|
|
26
|
+
# Unit test / coverage reports
|
|
27
|
+
htmlcov/
|
|
28
|
+
.tox/
|
|
29
|
+
.nox/
|
|
30
|
+
.coverage*
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.cache/
|
|
33
|
+
|
|
34
|
+
# Type checker / linter caches
|
|
35
|
+
.pyright/
|
|
36
|
+
.mypy_cache/
|
|
37
|
+
.ruff_cache/
|
|
38
|
+
pyrightconfig.json
|
|
39
|
+
|
|
40
|
+
# Editors
|
|
41
|
+
.vscode/
|
|
42
|
+
.idea/
|
|
43
|
+
|
|
44
|
+
# OS
|
|
45
|
+
.DS_Store
|
|
46
|
+
Thumbs.db
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the sg-query-cache authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sg-query-cache
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Query caching for Autodesk Flow Production Tracking
|
|
5
|
+
Project-URL: Documentation, https://github.com/dactylo/sg-query-cache/tree/main/docs
|
|
6
|
+
Project-URL: Homepage, https://github.com/dactylo/sg-query-cache
|
|
7
|
+
Project-URL: Issues, https://github.com/dactylo/sg-query-cache/issues
|
|
8
|
+
Project-URL: Repository, https://github.com/dactylo/sg-query-cache
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cache,flow-production-tracking,queries,shotgrid,shotgun,valkey
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: typing-extensions>=4.5
|
|
21
|
+
Provides-Extra: valkey
|
|
22
|
+
Requires-Dist: valkey<6.2,>=6.1.1; extra == 'valkey'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# sg-query-cache
|
|
26
|
+
|
|
27
|
+
sg-query-cache reduces repeated Autodesk Flow Production Tracking API reads from Python workers.
|
|
28
|
+
When render jobs, pipeline tools, or services ask the same question many times, the cache answers
|
|
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 and keeps its method
|
|
31
|
+
signatures, so your code changes only where it creates the client.
|
|
32
|
+
|
|
33
|
+
This documentation uses SG for Flow Production Tracking, formerly named Shotgun and ShotGrid.
|
|
34
|
+
|
|
35
|
+
Caching trades freshness for fewer requests. Every cached answer has a maximum age, 30 seconds by
|
|
36
|
+
default for a single process. Writes made through the wrapper invalidate the cached answers they
|
|
37
|
+
can affect, and a shared deployment also invalidates answers when SG reports a change. Until then,
|
|
38
|
+
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.1.0/docs/consistency.md) states the guarantee
|
|
40
|
+
in full.
|
|
41
|
+
|
|
42
|
+
## When to use it
|
|
43
|
+
|
|
44
|
+
The cache helps when processes repeat exact reads: the same method, filters, fields, and client
|
|
45
|
+
options, under the same SG permissions, within seconds or minutes of each other. It does not speed
|
|
46
|
+
up a read it has not seen before. It caches `find`, `find_one`, and schema reads, plus
|
|
47
|
+
`work_schedule_read` if you enable it; other reads go straight to SG.
|
|
48
|
+
|
|
49
|
+
Choose a layout:
|
|
50
|
+
|
|
51
|
+
| Layout | Use when | What it needs, and how fresh it is |
|
|
52
|
+
| --------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| Local (default) | One process repeats reads | Nothing beyond this package. Each process keeps its own in-memory cache, and changes made elsewhere appear within 30 seconds by default. |
|
|
54
|
+
| Shared | Several processes should reuse each other's results | Two Valkey services and the SG event-daemon plugin, which invalidates results as SG reports changes. Most results are kept up to 10 minutes by default. |
|
|
55
|
+
| Hierarchical | Shared results plus a fast per-process copy | The same as shared. Each process can serve its own copy for up to 10 seconds by default after another process changes the data. |
|
|
56
|
+
|
|
57
|
+
Start with the local layout to measure how many of your reads repeat. A shared layout trusts every
|
|
58
|
+
client that can reach its Valkey services: the cache does not check that a caller's declared
|
|
59
|
+
identity matches its SG credentials. Share Valkey only among processes you trust to declare their
|
|
60
|
+
identities correctly.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
sg-query-cache requires Python 3.11 or newer and an SG client in the same environment:
|
|
65
|
+
|
|
66
|
+
- For synchronous code, the official `shotgun_api3` package or `sg-python-api`.
|
|
67
|
+
- For native async code, `sg-python-api[async]`.
|
|
68
|
+
|
|
69
|
+
The official package and sg-python-api both provide the `shotgun_api3` import name, so install only
|
|
70
|
+
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.1.0/docs/operations.md#service-layout).
|
|
72
|
+
|
|
73
|
+
From a checkout, install the library into the environment that runs your SG client:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
python -m pip install .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
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 accepts valkey-py 6.1.x, the
|
|
81
|
+
release family this version is tested with:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
python -m pip install '.[valkey]'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick start
|
|
88
|
+
|
|
89
|
+
### Cache a synchronous client
|
|
90
|
+
|
|
91
|
+
Set `SHOTGUN_URL`, `SHOTGUN_SCRIPT_NAME`, and `SHOTGUN_API_KEY` for a script account with read
|
|
92
|
+
access, then run:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import os
|
|
96
|
+
from time import monotonic
|
|
97
|
+
|
|
98
|
+
from shotgun_api3 import Shotgun
|
|
99
|
+
from sg_query_cache import (
|
|
100
|
+
CacheSettings,
|
|
101
|
+
CachedShotgun,
|
|
102
|
+
ClientContext,
|
|
103
|
+
Identity,
|
|
104
|
+
LocalBackend,
|
|
105
|
+
NullMetrics,
|
|
106
|
+
QueryCache,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
site_url = os.environ['SHOTGUN_URL']
|
|
110
|
+
script_name = os.environ['SHOTGUN_SCRIPT_NAME']
|
|
111
|
+
raw_sg = Shotgun(site_url, script_name=script_name, api_key=os.environ['SHOTGUN_API_KEY'])
|
|
112
|
+
|
|
113
|
+
settings = CacheSettings()
|
|
114
|
+
identity = Identity(kind='script', issuer='shotgun-script', namespace=script_name)
|
|
115
|
+
cache = QueryCache(settings, identity, l1=LocalBackend(settings, monotonic, NullMetrics()))
|
|
116
|
+
context = ClientContext(
|
|
117
|
+
site_url=site_url,
|
|
118
|
+
site_namespace=settings.site_namespace,
|
|
119
|
+
localized=raw_sg.config.localized,
|
|
120
|
+
convert_datetimes_to_utc=raw_sg.config.convert_datetimes_to_utc,
|
|
121
|
+
records_per_page=raw_sg.config.records_per_page,
|
|
122
|
+
)
|
|
123
|
+
sg = CachedShotgun(raw_sg, cache, context)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def timed_read(label: str) -> None:
|
|
127
|
+
started = monotonic()
|
|
128
|
+
assets = sg.find('Asset', [['sg_status_list', 'is', 'ip']], ['code'])
|
|
129
|
+
print(f'{label}: {len(assets)} assets in {monotonic() - started:.3f} s')
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
timed_read('first read')
|
|
134
|
+
timed_read('second read')
|
|
135
|
+
with sg.bypass():
|
|
136
|
+
timed_read('bypass')
|
|
137
|
+
finally:
|
|
138
|
+
cache.close()
|
|
139
|
+
raw_sg.close()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You should see output like this, with your own counts and times:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
first read: 42 assets in 0.412 s
|
|
146
|
+
second read: 42 assets in 0.001 s
|
|
147
|
+
bypass: 42 assets in 0.198 s
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The first read goes to SG. It also loads the site schema, which the cache uses to work out what
|
|
151
|
+
each query depends on. The second, identical read comes from the in-process cache. Inside
|
|
152
|
+
`bypass()`, reads always go to SG and leave the cache unchanged. A new process starts with an empty
|
|
153
|
+
local cache.
|
|
154
|
+
|
|
155
|
+
The example sets up four pieces:
|
|
156
|
+
|
|
157
|
+
- `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.1.0/docs/configuration.md).
|
|
159
|
+
- `Identity` names the SG permission scope whose results may be reused. Here every process running
|
|
160
|
+
this script shares one identity named after the script; give scripts with different permissions
|
|
161
|
+
different identities. Use stable configuration values, never an API key, session token, or
|
|
162
|
+
process ID, because identity values become part of every cache key.
|
|
163
|
+
- `QueryCache` with a `LocalBackend` is the cache itself. The wrapper closes neither the cache nor
|
|
164
|
+
the SG client, so the example closes both.
|
|
165
|
+
- `ClientContext` records the raw client's response options, such as page size, so results fetched
|
|
166
|
+
with different options are never mixed. Keep it matched to the raw client's configuration.
|
|
167
|
+
|
|
168
|
+
### Use a native async client
|
|
169
|
+
|
|
170
|
+
With `sg-python-api[async]`, `AsyncCachedShotgun.open()` builds the cache from your settings and
|
|
171
|
+
closes it on exit. You open and close the SG client yourself:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import asyncio
|
|
175
|
+
import os
|
|
176
|
+
|
|
177
|
+
from shotgun_api3 import AsyncShotgun
|
|
178
|
+
from sg_query_cache import AsyncCachedShotgun, CacheSettings, ClientContext, Identity
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def main() -> None:
|
|
182
|
+
site_url = os.environ['SHOTGUN_URL']
|
|
183
|
+
script_name = os.environ['SHOTGUN_SCRIPT_NAME']
|
|
184
|
+
settings = CacheSettings()
|
|
185
|
+
identity = Identity(kind='script', issuer='shotgun-script', namespace=script_name)
|
|
186
|
+
async with AsyncShotgun(
|
|
187
|
+
site_url, script_name=script_name, api_key=os.environ['SHOTGUN_API_KEY']
|
|
188
|
+
) as raw_sg:
|
|
189
|
+
context = ClientContext(
|
|
190
|
+
site_url=site_url,
|
|
191
|
+
site_namespace=settings.site_namespace,
|
|
192
|
+
localized=raw_sg.config.localized,
|
|
193
|
+
convert_datetimes_to_utc=raw_sg.config.convert_datetimes_to_utc,
|
|
194
|
+
records_per_page=raw_sg.config.records_per_page,
|
|
195
|
+
)
|
|
196
|
+
async with AsyncCachedShotgun.open(raw_sg, settings, identity, context) as sg:
|
|
197
|
+
filters = [['sg_status_list', 'is', 'ip']]
|
|
198
|
+
for label in ('first read', 'second read'):
|
|
199
|
+
assets = await sg.find('Asset', filters, ['code'])
|
|
200
|
+
print(f'{label}: {len(assets)} assets')
|
|
201
|
+
with sg.bypass():
|
|
202
|
+
assets = await sg.find('Asset', filters, ['code'])
|
|
203
|
+
print(f'bypass: {len(assets)} assets')
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
asyncio.run(main())
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
As in the synchronous example, the second read comes from the cache and the bypassed read goes to
|
|
210
|
+
SG. `bypass()` is an ordinary `with` block, even in async code.
|
|
211
|
+
|
|
212
|
+
Use each async cache on one event loop. `bypass()` and other policy scopes apply to the current
|
|
213
|
+
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.1.0/docs/operations.md#async-deadlines-and-shutdown).
|
|
215
|
+
|
|
216
|
+
## Documentation
|
|
217
|
+
|
|
218
|
+
| If you want to... | Read |
|
|
219
|
+
| --------------------------------------------------- | ------------------------------------------------------------- |
|
|
220
|
+
| Understand how fresh results are and when to bypass | [Consistency](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/consistency.md) |
|
|
221
|
+
| Choose a layout and write the settings file | [Configuration](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/configuration.md) |
|
|
222
|
+
| Cache reads for human users | [Identities](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/configuration.md#identities-and-trust) |
|
|
223
|
+
| Change how long results are kept | [Maximum age rules](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/configuration.md#maximum-age-rules) |
|
|
224
|
+
| Run Valkey and the invalidation plugin | [Operations](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/operations.md) |
|
|
225
|
+
| Flush cached results after an incident | [Recovery](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/operations.md#flush-or-rotate-cached-results) |
|
|
226
|
+
| Collect metrics and set alerts | [Metrics](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/operations.md#metrics-and-alerts) |
|
|
227
|
+
| Measure savings for your workload | [Benchmarks](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/benchmarks.md) |
|
|
228
|
+
| See changes by version | [Changelog](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/CHANGELOG.md) |
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
See the [development guide](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/docs/development.md) for the required tools, CI, and dependency locks.
|
|
233
|
+
With those tools installed, run:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
make setup
|
|
237
|
+
make check
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The checks never contact SG or Valkey; live Valkey integration tests are separate and need
|
|
241
|
+
disposable services.
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
sg-query-cache is available under the [MIT License](https://github.com/dactylo/sg-query-cache/blob/v0.1.0/LICENSE). It is an independent project and is
|
|
246
|
+
not affiliated with or endorsed by Autodesk. Flow Production Tracking is a product of Autodesk,
|
|
247
|
+
Inc.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# sg-query-cache
|
|
2
|
+
|
|
3
|
+
sg-query-cache reduces repeated Autodesk Flow Production Tracking API reads from Python workers.
|
|
4
|
+
When render jobs, pipeline tools, or services ask the same question many times, the cache answers
|
|
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 and keeps its method
|
|
7
|
+
signatures, so your code changes only where it creates the client.
|
|
8
|
+
|
|
9
|
+
This documentation uses SG for Flow Production Tracking, formerly named Shotgun and ShotGrid.
|
|
10
|
+
|
|
11
|
+
Caching trades freshness for fewer requests. Every cached answer has a maximum age, 30 seconds by
|
|
12
|
+
default for a single process. Writes made through the wrapper invalidate the cached answers they
|
|
13
|
+
can affect, and a shared deployment also invalidates answers when SG reports a change. Until then,
|
|
14
|
+
a change made elsewhere can be missing from a cached answer. When a read must reflect SG right now,
|
|
15
|
+
bypass the cache for that read. The [consistency guide](docs/consistency.md) states the guarantee
|
|
16
|
+
in full.
|
|
17
|
+
|
|
18
|
+
## When to use it
|
|
19
|
+
|
|
20
|
+
The cache helps when processes repeat exact reads: the same method, filters, fields, and client
|
|
21
|
+
options, under the same SG permissions, within seconds or minutes of each other. It does not speed
|
|
22
|
+
up a read it has not seen before. It caches `find`, `find_one`, and schema reads, plus
|
|
23
|
+
`work_schedule_read` if you enable it; other reads go straight to SG.
|
|
24
|
+
|
|
25
|
+
Choose a layout:
|
|
26
|
+
|
|
27
|
+
| Layout | Use when | What it needs, and how fresh it is |
|
|
28
|
+
| --------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| Local (default) | One process repeats reads | Nothing beyond this package. Each process keeps its own in-memory cache, and changes made elsewhere appear within 30 seconds by default. |
|
|
30
|
+
| Shared | Several processes should reuse each other's results | Two Valkey services and the SG event-daemon plugin, which invalidates results as SG reports changes. Most results are kept up to 10 minutes by default. |
|
|
31
|
+
| Hierarchical | Shared results plus a fast per-process copy | The same as shared. Each process can serve its own copy for up to 10 seconds by default after another process changes the data. |
|
|
32
|
+
|
|
33
|
+
Start with the local layout to measure how many of your reads repeat. A shared layout trusts every
|
|
34
|
+
client that can reach its Valkey services: the cache does not check that a caller's declared
|
|
35
|
+
identity matches its SG credentials. Share Valkey only among processes you trust to declare their
|
|
36
|
+
identities correctly.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
sg-query-cache requires Python 3.11 or newer and an SG client in the same environment:
|
|
41
|
+
|
|
42
|
+
- For synchronous code, the official `shotgun_api3` package or `sg-python-api`.
|
|
43
|
+
- For native async code, `sg-python-api[async]`.
|
|
44
|
+
|
|
45
|
+
The official package and sg-python-api both provide the `shotgun_api3` import name, so install only
|
|
46
|
+
one of them. The shared and hierarchical layouts also need two Valkey services; see
|
|
47
|
+
[service layout](docs/operations.md#service-layout).
|
|
48
|
+
|
|
49
|
+
From a checkout, install the library into the environment that runs your SG client:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
python -m pip install .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
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 accepts valkey-py 6.1.x, the
|
|
57
|
+
release family this version is tested with:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
python -m pip install '.[valkey]'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
### Cache a synchronous client
|
|
66
|
+
|
|
67
|
+
Set `SHOTGUN_URL`, `SHOTGUN_SCRIPT_NAME`, and `SHOTGUN_API_KEY` for a script account with read
|
|
68
|
+
access, then run:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import os
|
|
72
|
+
from time import monotonic
|
|
73
|
+
|
|
74
|
+
from shotgun_api3 import Shotgun
|
|
75
|
+
from sg_query_cache import (
|
|
76
|
+
CacheSettings,
|
|
77
|
+
CachedShotgun,
|
|
78
|
+
ClientContext,
|
|
79
|
+
Identity,
|
|
80
|
+
LocalBackend,
|
|
81
|
+
NullMetrics,
|
|
82
|
+
QueryCache,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
site_url = os.environ['SHOTGUN_URL']
|
|
86
|
+
script_name = os.environ['SHOTGUN_SCRIPT_NAME']
|
|
87
|
+
raw_sg = Shotgun(site_url, script_name=script_name, api_key=os.environ['SHOTGUN_API_KEY'])
|
|
88
|
+
|
|
89
|
+
settings = CacheSettings()
|
|
90
|
+
identity = Identity(kind='script', issuer='shotgun-script', namespace=script_name)
|
|
91
|
+
cache = QueryCache(settings, identity, l1=LocalBackend(settings, monotonic, NullMetrics()))
|
|
92
|
+
context = ClientContext(
|
|
93
|
+
site_url=site_url,
|
|
94
|
+
site_namespace=settings.site_namespace,
|
|
95
|
+
localized=raw_sg.config.localized,
|
|
96
|
+
convert_datetimes_to_utc=raw_sg.config.convert_datetimes_to_utc,
|
|
97
|
+
records_per_page=raw_sg.config.records_per_page,
|
|
98
|
+
)
|
|
99
|
+
sg = CachedShotgun(raw_sg, cache, context)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def timed_read(label: str) -> None:
|
|
103
|
+
started = monotonic()
|
|
104
|
+
assets = sg.find('Asset', [['sg_status_list', 'is', 'ip']], ['code'])
|
|
105
|
+
print(f'{label}: {len(assets)} assets in {monotonic() - started:.3f} s')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
timed_read('first read')
|
|
110
|
+
timed_read('second read')
|
|
111
|
+
with sg.bypass():
|
|
112
|
+
timed_read('bypass')
|
|
113
|
+
finally:
|
|
114
|
+
cache.close()
|
|
115
|
+
raw_sg.close()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
You should see output like this, with your own counts and times:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
first read: 42 assets in 0.412 s
|
|
122
|
+
second read: 42 assets in 0.001 s
|
|
123
|
+
bypass: 42 assets in 0.198 s
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The first read goes to SG. It also loads the site schema, which the cache uses to work out what
|
|
127
|
+
each query depends on. The second, identical read comes from the in-process cache. Inside
|
|
128
|
+
`bypass()`, reads always go to SG and leave the cache unchanged. A new process starts with an empty
|
|
129
|
+
local cache.
|
|
130
|
+
|
|
131
|
+
The example sets up four pieces:
|
|
132
|
+
|
|
133
|
+
- `CacheSettings()` selects the local layout with default limits. Shared layouts load their
|
|
134
|
+
settings from a TOML file; see [configuration](docs/configuration.md).
|
|
135
|
+
- `Identity` names the SG permission scope whose results may be reused. Here every process running
|
|
136
|
+
this script shares one identity named after the script; give scripts with different permissions
|
|
137
|
+
different identities. Use stable configuration values, never an API key, session token, or
|
|
138
|
+
process ID, because identity values become part of every cache key.
|
|
139
|
+
- `QueryCache` with a `LocalBackend` is the cache itself. The wrapper closes neither the cache nor
|
|
140
|
+
the SG client, so the example closes both.
|
|
141
|
+
- `ClientContext` records the raw client's response options, such as page size, so results fetched
|
|
142
|
+
with different options are never mixed. Keep it matched to the raw client's configuration.
|
|
143
|
+
|
|
144
|
+
### Use a native async client
|
|
145
|
+
|
|
146
|
+
With `sg-python-api[async]`, `AsyncCachedShotgun.open()` builds the cache from your settings and
|
|
147
|
+
closes it on exit. You open and close the SG client yourself:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
import asyncio
|
|
151
|
+
import os
|
|
152
|
+
|
|
153
|
+
from shotgun_api3 import AsyncShotgun
|
|
154
|
+
from sg_query_cache import AsyncCachedShotgun, CacheSettings, ClientContext, Identity
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def main() -> None:
|
|
158
|
+
site_url = os.environ['SHOTGUN_URL']
|
|
159
|
+
script_name = os.environ['SHOTGUN_SCRIPT_NAME']
|
|
160
|
+
settings = CacheSettings()
|
|
161
|
+
identity = Identity(kind='script', issuer='shotgun-script', namespace=script_name)
|
|
162
|
+
async with AsyncShotgun(
|
|
163
|
+
site_url, script_name=script_name, api_key=os.environ['SHOTGUN_API_KEY']
|
|
164
|
+
) as raw_sg:
|
|
165
|
+
context = ClientContext(
|
|
166
|
+
site_url=site_url,
|
|
167
|
+
site_namespace=settings.site_namespace,
|
|
168
|
+
localized=raw_sg.config.localized,
|
|
169
|
+
convert_datetimes_to_utc=raw_sg.config.convert_datetimes_to_utc,
|
|
170
|
+
records_per_page=raw_sg.config.records_per_page,
|
|
171
|
+
)
|
|
172
|
+
async with AsyncCachedShotgun.open(raw_sg, settings, identity, context) as sg:
|
|
173
|
+
filters = [['sg_status_list', 'is', 'ip']]
|
|
174
|
+
for label in ('first read', 'second read'):
|
|
175
|
+
assets = await sg.find('Asset', filters, ['code'])
|
|
176
|
+
print(f'{label}: {len(assets)} assets')
|
|
177
|
+
with sg.bypass():
|
|
178
|
+
assets = await sg.find('Asset', filters, ['code'])
|
|
179
|
+
print(f'bypass: {len(assets)} assets')
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
asyncio.run(main())
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
As in the synchronous example, the second read comes from the cache and the bypassed read goes to
|
|
186
|
+
SG. `bypass()` is an ordinary `with` block, even in async code.
|
|
187
|
+
|
|
188
|
+
Use each async cache on one event loop. `bypass()` and other policy scopes apply to the current
|
|
189
|
+
task, and a task created inside a scope inherits it. Before you close caches in a service with
|
|
190
|
+
long-running tasks, read [async shutdown](docs/operations.md#async-deadlines-and-shutdown).
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
| If you want to... | Read |
|
|
195
|
+
| --------------------------------------------------- | ------------------------------------------------------------- |
|
|
196
|
+
| Understand how fresh results are and when to bypass | [Consistency](docs/consistency.md) |
|
|
197
|
+
| Choose a layout and write the settings file | [Configuration](docs/configuration.md) |
|
|
198
|
+
| Cache reads for human users | [Identities](docs/configuration.md#identities-and-trust) |
|
|
199
|
+
| Change how long results are kept | [Maximum age rules](docs/configuration.md#maximum-age-rules) |
|
|
200
|
+
| Run Valkey and the invalidation plugin | [Operations](docs/operations.md) |
|
|
201
|
+
| Flush cached results after an incident | [Recovery](docs/operations.md#flush-or-rotate-cached-results) |
|
|
202
|
+
| Collect metrics and set alerts | [Metrics](docs/operations.md#metrics-and-alerts) |
|
|
203
|
+
| Measure savings for your workload | [Benchmarks](docs/benchmarks.md) |
|
|
204
|
+
| See changes by version | [Changelog](CHANGELOG.md) |
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
See the [development guide](docs/development.md) for the required tools, CI, and dependency locks.
|
|
209
|
+
With those tools installed, run:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
make setup
|
|
213
|
+
make check
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The checks never contact SG or Valkey; live Valkey integration tests are separate and need
|
|
217
|
+
disposable services.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
sg-query-cache is available under the [MIT License](LICENSE). It is an independent project and is
|
|
222
|
+
not affiliated with or endorsed by Autodesk. Flow Production Tracking is a product of Autodesk,
|
|
223
|
+
Inc.
|