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.
Files changed (42) hide show
  1. sg_query_cache-0.1.0/.gitignore +46 -0
  2. sg_query_cache-0.1.0/LICENSE +21 -0
  3. sg_query_cache-0.1.0/PKG-INFO +247 -0
  4. sg_query_cache-0.1.0/README.md +223 -0
  5. sg_query_cache-0.1.0/pyproject.toml +175 -0
  6. sg_query_cache-0.1.0/src/sg_query_cache/__init__.py +39 -0
  7. sg_query_cache-0.1.0/src/sg_query_cache/__main__.py +8 -0
  8. sg_query_cache-0.1.0/src/sg_query_cache/_async_backend.py +101 -0
  9. sg_query_cache-0.1.0/src/sg_query_cache/_async_cache.py +1240 -0
  10. sg_query_cache-0.1.0/src/sg_query_cache/_async_client.py +1390 -0
  11. sg_query_cache-0.1.0/src/sg_query_cache/_async_local.py +318 -0
  12. sg_query_cache-0.1.0/src/sg_query_cache/_async_support.py +314 -0
  13. sg_query_cache-0.1.0/src/sg_query_cache/_async_valkey_control.py +580 -0
  14. sg_query_cache-0.1.0/src/sg_query_cache/_backend.py +168 -0
  15. sg_query_cache-0.1.0/src/sg_query_cache/_backend_validation.py +110 -0
  16. sg_query_cache-0.1.0/src/sg_query_cache/_cache.py +1608 -0
  17. sg_query_cache-0.1.0/src/sg_query_cache/_cache_common.py +387 -0
  18. sg_query_cache-0.1.0/src/sg_query_cache/_cli.py +313 -0
  19. sg_query_cache-0.1.0/src/sg_query_cache/_client.py +1061 -0
  20. sg_query_cache-0.1.0/src/sg_query_cache/_client_common.py +600 -0
  21. sg_query_cache-0.1.0/src/sg_query_cache/_codec.py +345 -0
  22. sg_query_cache-0.1.0/src/sg_query_cache/_config.py +1217 -0
  23. sg_query_cache-0.1.0/src/sg_query_cache/_dependencies.py +888 -0
  24. sg_query_cache-0.1.0/src/sg_query_cache/_downstream.py +141 -0
  25. sg_query_cache-0.1.0/src/sg_query_cache/_event_mapping.py +409 -0
  26. sg_query_cache-0.1.0/src/sg_query_cache/_keys.py +298 -0
  27. sg_query_cache-0.1.0/src/sg_query_cache/_local.py +301 -0
  28. sg_query_cache-0.1.0/src/sg_query_cache/_local_store.py +604 -0
  29. sg_query_cache-0.1.0/src/sg_query_cache/_metrics.py +200 -0
  30. sg_query_cache-0.1.0/src/sg_query_cache/_model.py +546 -0
  31. sg_query_cache-0.1.0/src/sg_query_cache/_policy.py +378 -0
  32. sg_query_cache-0.1.0/src/sg_query_cache/_query.py +53 -0
  33. sg_query_cache-0.1.0/src/sg_query_cache/_urls.py +324 -0
  34. sg_query_cache-0.1.0/src/sg_query_cache/_validation.py +70 -0
  35. sg_query_cache-0.1.0/src/sg_query_cache/_valkey_common.py +1037 -0
  36. sg_query_cache-0.1.0/src/sg_query_cache/_valkey_control.py +437 -0
  37. sg_query_cache-0.1.0/src/sg_query_cache/_valkey_proof.py +64 -0
  38. sg_query_cache-0.1.0/src/sg_query_cache/_valkey_scripts.py +454 -0
  39. sg_query_cache-0.1.0/src/sg_query_cache/daemon_plugin.py +159 -0
  40. sg_query_cache-0.1.0/src/sg_query_cache/py.typed +0 -0
  41. sg_query_cache-0.1.0/src/sg_query_cache/valkey.py +1533 -0
  42. 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.