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.
Files changed (43) hide show
  1. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/PKG-INFO +29 -33
  2. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/README.md +13 -17
  3. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/pyproject.toml +6 -15
  4. sg_query_cache-0.2.0/src/sg_query_cache/_async_backend.py +181 -0
  5. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_cache.py +375 -275
  6. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_client.py +741 -502
  7. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_local.py +18 -143
  8. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_support.py +74 -27
  9. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_async_valkey_control.py +152 -32
  10. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_backend.py +27 -2
  11. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_backend_validation.py +6 -6
  12. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_cache.py +635 -724
  13. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_cache_common.py +9 -14
  14. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_cli.py +124 -63
  15. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_client.py +661 -483
  16. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_client_common.py +382 -195
  17. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_config.py +299 -209
  18. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_dependencies.py +135 -92
  19. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_downstream.py +6 -2
  20. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_event_mapping.py +49 -59
  21. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_local.py +10 -5
  22. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_local_store.py +126 -46
  23. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_metrics.py +9 -1
  24. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_model.py +4 -11
  25. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_policy.py +8 -18
  26. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_query.py +12 -0
  27. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_urls.py +2 -9
  28. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_validation.py +3 -2
  29. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_common.py +41 -39
  30. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_control.py +88 -13
  31. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_scripts.py +128 -21
  32. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/daemon_plugin.py +22 -34
  33. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/valkey.py +359 -204
  34. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/valkey_async.py +936 -300
  35. sg_query_cache-0.1.0/src/sg_query_cache/_async_backend.py +0 -101
  36. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/.gitignore +0 -0
  37. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/LICENSE +0 -0
  38. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/__init__.py +0 -0
  39. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/__main__.py +0 -0
  40. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_codec.py +0 -0
  41. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_keys.py +1 -1
  42. {sg_query_cache-0.1.0 → sg_query_cache-0.2.0}/src/sg_query_cache/_valkey_proof.py +0 -0
  43. {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.1.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 and keeps its method
31
- signatures, so your code changes only where it creates the client.
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.1.0/docs/consistency.md) states the guarantee
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.1.0/docs/operations.md#service-layout).
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 accepts valkey-py 6.1.x, the
81
- release family this version is tested with:
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
- timed_read('first read')
134
- timed_read('second read')
135
- with sg.bypass():
136
- timed_read('bypass')
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.1.0/docs/configuration.md).
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
- - `QueryCache` with a `LocalBackend` is the cache itself. The wrapper closes neither the cache nor
164
- the SG client, so the example closes both.
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.1.0/docs/operations.md#async-deadlines-and-shutdown).
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.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) |
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.1.0/docs/development.md) for the required tools, CI, and dependency locks.
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.1.0/LICENSE). It is an independent project and is
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 and keeps its method
7
- signatures, so your code changes only where it creates the client.
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 accepts valkey-py 6.1.x, the
57
- release family this version is tested with:
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
- timed_read('first read')
110
- timed_read('second read')
111
- with sg.bypass():
112
- timed_read('bypass')
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
- - `QueryCache` with a `LocalBackend` is the cache itself. The wrapper closes neither the cache nor
140
- the SG client, so the example closes both.
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.1.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
- "SLF001",
106
- "T201", # print() found
105
+ "T201", # print() found
107
106
  ]
108
107
  lint.per-file-ignores."benchmarks/query_cache.py" = [
109
- "SLF001",
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
+ ...