pystackquery 1.0.1__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 (46) hide show
  1. pystackquery-1.0.1/.github/workflows/publish.yml +55 -0
  2. pystackquery-1.0.1/.gitignore +203 -0
  3. pystackquery-1.0.1/.python-version +1 -0
  4. pystackquery-1.0.1/CONTRIBUTING.md +48 -0
  5. pystackquery-1.0.1/PKG-INFO +24 -0
  6. pystackquery-1.0.1/README.md +156 -0
  7. pystackquery-1.0.1/RELEASING.md +44 -0
  8. pystackquery-1.0.1/docs/advanced-patterns.md +69 -0
  9. pystackquery-1.0.1/docs/api-reference.md +96 -0
  10. pystackquery-1.0.1/docs/cache-management.md +38 -0
  11. pystackquery-1.0.1/docs/framework-integrations.md +86 -0
  12. pystackquery-1.0.1/docs/getting-started.md +63 -0
  13. pystackquery-1.0.1/docs/mutations.md +58 -0
  14. pystackquery-1.0.1/docs/observers.md +73 -0
  15. pystackquery-1.0.1/docs/query-options.md +43 -0
  16. pystackquery-1.0.1/docs/storage-backends.md +127 -0
  17. pystackquery-1.0.1/examples/__init__.py +1 -0
  18. pystackquery-1.0.1/examples/background_worker.py +378 -0
  19. pystackquery-1.0.1/examples/benchmark.py +387 -0
  20. pystackquery-1.0.1/examples/demo.py +153 -0
  21. pystackquery-1.0.1/examples/fastapi_app.py +447 -0
  22. pystackquery-1.0.1/examples/redis_integration.py +122 -0
  23. pystackquery-1.0.1/examples/tkinter_app.py +442 -0
  24. pystackquery-1.0.1/pyproject.toml +57 -0
  25. pystackquery-1.0.1/pystackquery/__init__.py +77 -0
  26. pystackquery-1.0.1/pystackquery/cache.py +135 -0
  27. pystackquery-1.0.1/pystackquery/client.py +219 -0
  28. pystackquery-1.0.1/pystackquery/convenience.py +159 -0
  29. pystackquery-1.0.1/pystackquery/helpers.py +63 -0
  30. pystackquery-1.0.1/pystackquery/mutation.py +124 -0
  31. pystackquery-1.0.1/pystackquery/observer.py +128 -0
  32. pystackquery-1.0.1/pystackquery/options.py +101 -0
  33. pystackquery-1.0.1/pystackquery/query.py +339 -0
  34. pystackquery-1.0.1/pystackquery/state.py +247 -0
  35. pystackquery-1.0.1/pystackquery/types.py +80 -0
  36. pystackquery-1.0.1/tests/__init__.py +0 -0
  37. pystackquery-1.0.1/tests/conftest.py +48 -0
  38. pystackquery-1.0.1/tests/core/__init__.py +0 -0
  39. pystackquery-1.0.1/tests/core/test_client.py +98 -0
  40. pystackquery-1.0.1/tests/core/test_query.py +74 -0
  41. pystackquery-1.0.1/tests/features/__init__.py +0 -0
  42. pystackquery-1.0.1/tests/features/test_infrastructure.py +103 -0
  43. pystackquery-1.0.1/tests/features/test_mutations.py +71 -0
  44. pystackquery-1.0.1/tests/features/test_observers.py +84 -0
  45. pystackquery-1.0.1/tests/features/test_persistence.py +90 -0
  46. pystackquery-1.0.1/uv.lock +753 -0
@@ -0,0 +1,55 @@
1
+ name: Publish to PyPI and GitHub Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ test:
13
+ name: Run Tests
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+ - name: Install dependencies
22
+ run: |
23
+ pip install uv
24
+ uv sync --all-extras --dev
25
+ - name: Run Tests
26
+ run: uv run pytest
27
+
28
+ build-and-publish:
29
+ name: Build and Publish
30
+ needs: test
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - name: Set up Python
36
+ uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.12"
39
+
40
+ - name: Install build tools
41
+ run: pip install build
42
+
43
+ - name: Build package
44
+ run: python -m build
45
+
46
+ - name: Publish to PyPI
47
+ uses: pypa/gh-action-pypi-publish@release/v1
48
+ with:
49
+ password: ${{ secrets.PYPI_API_TOKEN }}
50
+
51
+ - name: Create GitHub Release
52
+ uses: softprops/action-gh-release@v2
53
+ with:
54
+ generate_release_notes: true
55
+ files: dist/*
@@ -0,0 +1,203 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python,pythonvanilla
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python,pythonvanilla
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ ### PythonVanilla ###
177
+ # Byte-compiled / optimized / DLL files
178
+
179
+ # C extensions
180
+
181
+ # Distribution / packaging
182
+
183
+ # Installer logs
184
+
185
+ # Unit test / coverage reports
186
+
187
+ # Translations
188
+
189
+ # pyenv
190
+ # For a library or package, you might want to ignore these files since the code is
191
+ # intended to run in multiple environments; otherwise, check them in:
192
+ # .python-version
193
+
194
+ # pipenv
195
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
196
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
197
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
198
+ # install all needed dependencies.
199
+
200
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
201
+
202
+
203
+ # End of https://www.toptal.com/developers/gitignore/api/python,pythonvanilla
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,48 @@
1
+ # Contributing to PyStackQuery
2
+
3
+ First off, thank you for considering contributing to PyStackQuery! It's people like you that make PyStackQuery such a great tool.
4
+
5
+ ## Code of Conduct
6
+
7
+ By participating in this project, you are expected to uphold our high standards of professional conduct and technical excellence.
8
+
9
+ ## How Can I Contribute?
10
+
11
+ ### Reporting Bugs
12
+ * **Check the existing issues** to see if the bug has already been reported.
13
+ * If you can't find an open issue that addresses the problem, **open a new one**.
14
+ * Include a **reproducible example** and details about your environment (OS, Python version).
15
+
16
+ ### Suggesting Enhancements
17
+ * Open an issue to discuss the change before starting work.
18
+ * Explain why the feature would be useful to most users.
19
+
20
+ ### Pull Requests
21
+ 1. Fork the repo and create your branch from `main`.
22
+ 2. Install dependencies using `uv sync --dev`.
23
+ 3. If you've added code that should be tested, add tests.
24
+ 4. Ensure the test suite passes (`uv run pytest`).
25
+ 5. Run linting and type checking (`uv run ruff check .` and `uv run mypy .`).
26
+ 6. Ensure your code adheres to the existing style and architecture (PEP 695 generics, PEP 8).
27
+
28
+ ## Development Setup
29
+
30
+ We use `uv` for dependency management.
31
+
32
+ ```bash
33
+ # Clone the repo
34
+ git clone https://github.com/yourusername/pystackquery.git
35
+ cd pystackquery
36
+
37
+ # Install dependencies
38
+ uv sync --dev
39
+
40
+ # Run tests
41
+ uv run pytest
42
+ ```
43
+
44
+ ## Styling & Standards
45
+ * **Python 3.12+**: Use modern syntax (Built-in generics, `|` unions).
46
+ * **Linting**: We use Ruff. Always run `ruff check --fix .` before committing.
47
+ * **Typing**: Static typing is mandatory. Ensure `mypy .` returns zero errors.
48
+ * **Documentation**: Update documentation in `docs/` if you change any public APIs.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: pystackquery
3
+ Version: 1.0.1
4
+ Summary: Async data fetching and caching library for Python
5
+ License-Expression: MIT
6
+ Keywords: async,cache,data-fetching,query,state-management
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Framework :: AsyncIO
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: typing-extensions>=4.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: aiohttp>=3.9; extra == 'dev'
21
+ Requires-Dist: mypy>=1.9; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.3; extra == 'dev'
@@ -0,0 +1,156 @@
1
+ # PyStackQuery
2
+
3
+ Async data fetching and caching library for Python.
4
+
5
+ PyStackQuery handles the hard parts of working with async data: caching, deduplication, retries, and reactive state updates. You focus on *what* to fetch, the library handles *how*.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install pystackquery
11
+ ```
12
+
13
+ Requires Python 3.11+
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ import asyncio
19
+ from pystackquery import QueryClient, QueryOptions
20
+
21
+ client = QueryClient()
22
+
23
+ async def fetch_user(user_id: int) -> dict:
24
+ # Your async fetch logic here
25
+ async with aiohttp.ClientSession() as session:
26
+ async with session.get(f"https://api.example.com/users/{user_id}") as resp:
27
+ return await resp.json()
28
+
29
+ async def main():
30
+ # Fetch with automatic caching
31
+ user = await client.fetch_query(
32
+ QueryOptions(
33
+ query_key=("user", "123"),
34
+ query_fn=lambda: fetch_user(123)
35
+ )
36
+ )
37
+
38
+ # Second call returns cached data instantly
39
+ user_again = await client.fetch_query(
40
+ QueryOptions(
41
+ query_key=("user", "123"),
42
+ query_fn=lambda: fetch_user(123)
43
+ )
44
+ )
45
+
46
+ asyncio.run(main())
47
+ ```
48
+
49
+ That's it. The first call fetches from the API. The second call returns instantly from cache.
50
+
51
+ ## What Problems Does This Solve?
52
+
53
+ **Without PyStackQuery**, you write code like this over and over:
54
+
55
+ ```python
56
+ cache = {}
57
+ pending = {}
58
+ lock = asyncio.Lock()
59
+
60
+ async def get_user(user_id):
61
+ key = f"user_{user_id}"
62
+
63
+ async with lock:
64
+ if key in cache:
65
+ return cache[key]
66
+ if key in pending:
67
+ return await pending[key]
68
+
69
+ task = asyncio.create_task(fetch_user(user_id))
70
+ pending[key] = task
71
+
72
+ try:
73
+ result = await task
74
+ cache[key] = result
75
+ return result
76
+ finally:
77
+ del pending[key]
78
+ ```
79
+
80
+ **With PyStackQuery**, you write:
81
+
82
+ ```python
83
+ user = await client.fetch_query(
84
+ QueryOptions(("user", user_id), lambda: fetch_user(user_id))
85
+ )
86
+ ```
87
+
88
+ The library handles:
89
+ - Caching
90
+ - Request deduplication (concurrent calls share one request)
91
+ - Automatic retries with backoff
92
+ - Stale-while-revalidate
93
+ - Cache invalidation
94
+ - Reactive updates
95
+
96
+ ## Core Concepts
97
+
98
+ ### Query Keys
99
+
100
+ Every query needs a unique key. Keys are tuples of strings:
101
+
102
+ ```python
103
+ ("users",) # All users
104
+ ("user", "123") # Specific user
105
+ ("posts", "user", "123") # Posts by user 123
106
+ ```
107
+
108
+ Keys enable:
109
+ - Cache lookups
110
+ - Partial invalidation (invalidate `("users",)` clears all user queries)
111
+
112
+ ### Query Options
113
+
114
+ Configure how a query behaves:
115
+
116
+ ```python
117
+ QueryOptions(
118
+ query_key=("user", "123"),
119
+ query_fn=lambda: fetch_user(123),
120
+ stale_time=60.0, # Data fresh for 60 seconds
121
+ retry=3, # Retry 3 times on failure
122
+ )
123
+ ```
124
+
125
+ ### Stale-While-Revalidate
126
+
127
+ When data becomes stale, you get the cached data immediately while a background refresh happens:
128
+
129
+ ```python
130
+ # First call: fetches from API
131
+ data = await client.fetch_query(opts)
132
+
133
+ # Wait for stale_time to pass...
134
+
135
+ # Second call: returns stale data instantly, refreshes in background
136
+ data = await client.fetch_query(opts)
137
+ ```
138
+
139
+ Your users see data immediately. Fresh data loads in the background.
140
+
141
+ ## Documentation
142
+
143
+ See the [docs/](./docs/) folder for comprehensive documentation:
144
+
145
+ - [Getting Started](./docs/getting-started.md) - Installation and basic usage
146
+ - [Query Options](./docs/query-options.md) - All configuration options explained
147
+ - [Mutations](./docs/mutations.md) - Handling POST/PUT/DELETE operations
148
+ - [Cache Management](./docs/cache-management.md) - Invalidation, prefetching, manual updates
149
+ - [Observers](./docs/observers.md) - Reactive state updates
150
+ - [Advanced Patterns](./docs/advanced-patterns.md) - Dependent queries, parallel fetching
151
+ - [Framework Integrations](./docs/framework-integrations.md) - FastAPI, Tkinter, Textual, CLI tools, Jupyter
152
+ - [API Reference](./docs/api-reference.md) - Complete API documentation
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,44 @@
1
+ # Release Process
2
+
3
+ This project uses GitHub Actions to automate publishing to PyPI and creating GitHub Releases.
4
+
5
+ ## Prerequisites
6
+ 1. **PyPI API Token**: Ensure `PYPI_API_TOKEN` is set in the GitHub Repository Secrets.
7
+ 2. **Clean State**: Ensure all tests, linting, and type checks pass.
8
+
9
+ ## Step-by-Step Release
10
+
11
+ ### 1. Update the Version
12
+ Modify the `version` field in `pyproject.toml`.
13
+
14
+ ```toml
15
+ [project]
16
+ name = "pystackquery"
17
+ version = "0.1.0" # Update this!
18
+ ```
19
+
20
+ ### 2. Commit the change
21
+ ```bash
22
+ git add pyproject.toml
23
+ git commit -m "chore: bump version to 0.1.0"
24
+ git push origin main
25
+ ```
26
+
27
+ ### 3. Tag and Push
28
+ Creating a tag that matches the pattern `v*.*.*` will trigger the `Publish` workflow.
29
+
30
+ ```bash
31
+ # Create the tag locally
32
+ git tag v0.1.0
33
+
34
+ # Push the tag to GitHub
35
+ git push origin v0.1.0
36
+ ```
37
+
38
+ ## What Happens Automatically?
39
+ Once the tag is pushed:
40
+ 1. **Test Job**: GitHub Actions runs the full test suite.
41
+ 2. **Publish Job**: If tests pass, it builds the package and uploads it to PyPI.
42
+ 3. **Release Job**: A GitHub Release is created automatically with:
43
+ * Automatic release notes (based on commit messages).
44
+ * Built wheel and source distribution files attached.
@@ -0,0 +1,69 @@
1
+ # Advanced Patterns
2
+
3
+ This guide explores architectural patterns for scaling PyStackQuery in complex applications.
4
+
5
+ ## Prefetching for Speed
6
+
7
+ Anticipate user navigation by warming up the cache. Prefetching is silent and never throws on network failure.
8
+
9
+ ```python
10
+ async def on_user_hover(user_id):
11
+ # Start fetching data before the user even clicks
12
+ await client.prefetch_query(
13
+ QueryOptions(query_key=("user", user_id), query_fn=fetch_user)
14
+ )
15
+ ```
16
+
17
+ ## Parallel and Dependent Queries
18
+
19
+ ### Parallel
20
+ Use `parallel_queries` to reduce total latency when a screen needs data from multiple sources.
21
+
22
+ ```python
23
+ from pystackquery import parallel_queries
24
+
25
+ # Fetches all three concurrently
26
+ users, settings, posts = await parallel_queries(client, opt1, opt2, opt3)
27
+ ```
28
+
29
+ ### Dependent
30
+ Use `dependent_query` when the second request requires an ID from the first.
31
+
32
+ ```python
33
+ from pystackquery import dependent_query
34
+
35
+ posts = await dependent_query(
36
+ client,
37
+ depends_on=QueryOptions(("user", "me"), fetch_me),
38
+ then=lambda user: QueryOptions(("posts", user["id"]), fetch_posts)
39
+ )
40
+ ```
41
+
42
+ ## Testing Observers
43
+
44
+ Since PyStackQuery uses background tasks for hydration and refetching, your tests should account for event loop cycles.
45
+
46
+ ```python
47
+ async def test_observer_flow(client):
48
+ observer = client.watch(opts)
49
+ states = []
50
+
51
+ # Subscribe is synchronous
52
+ unsub = observer.subscribe(lambda s: states.append(s.status))
53
+
54
+ # Wait for the background fetch to settle
55
+ await asyncio.sleep(0.1)
56
+
57
+ assert QueryStatus.SUCCESS in states
58
+ unsub()
59
+ ```
60
+
61
+ ## Production L2 Backends
62
+
63
+ For production, we recommend implementing a robust `StorageBackend` using Redis or SQLite.
64
+
65
+ ### Why SQLite?
66
+ For single-server or desktop applications, SQLite is often faster than Redis and requires zero infrastructure management. It’s perfect for ensuring data persists across application restarts.
67
+
68
+ ### Why Redis?
69
+ Use Redis if you have multiple server instances (distributed system) that need to share the same cached state to stay synchronized.
@@ -0,0 +1,96 @@
1
+ # API Reference
2
+
3
+ PyStackQuery is designed to be minimal but powerful. The entire public API revolves around the `QueryClient`.
4
+
5
+ ## QueryClient
6
+
7
+ The main orchestrator for the dual-tier cache.
8
+
9
+ ### fetch_query
10
+
11
+ ```python
12
+ async def fetch_query[T](self, options: QueryOptions[T]) -> T
13
+ ```
14
+
15
+ The standard way to request data. Uses the **Stale-While-Revalidate** pattern:
16
+ 1. **Fresh L1/L2:** Returns data instantly.
17
+ 2. **Stale L1/L2:** Returns stale data instantly, triggers background network fetch.
18
+ 3. **Cold Cache:** Performs blocking network fetch.
19
+
20
+ ### prefetch_query
21
+
22
+ ```python
23
+ async def prefetch_query[T](self, options: QueryOptions[T]) -> None
24
+ ```
25
+
26
+ Warms up the cache for a specific key. Unlike `fetch_query`, this never blocks for the result and fails silently on network errors.
27
+
28
+ ### watch
29
+
30
+ ```python
31
+ def watch[T](self, options: QueryOptions[T]) -> QueryObserver[T]
32
+ ```
33
+
34
+ Creates a reactive observer. Use this when you need your UI or service to react to data changes over time.
35
+
36
+ ---
37
+
38
+ ## QueryObserver
39
+
40
+ The bridge between a Query and a listener.
41
+
42
+ ### subscribe
43
+
44
+ ```python
45
+ def subscribe(self, listener: Callable[[QueryState[T, Exception]], object]) -> Callable[[], None]
46
+ ```
47
+
48
+ **Synchronous.** Attaches a callback to the query.
49
+ - The `listener` is called immediately with the current state.
50
+ - If the query is stale, a background fetch is initiated.
51
+ - Returns an **unsubscribe function**.
52
+
53
+ ---
54
+
55
+ ## Persistence and Hydration
56
+
57
+ PyStackQuery supports pluggable L2 storage (Redis, SQLite, Disk, etc.).
58
+
59
+ ### How Hydration Works
60
+ 1. When you call `watch()` or `fetch_query()` on a cold key, the client creates a Query instance and starts a background **Hydration Task**.
61
+ 2. `fetch_query()` will `await` this task before checking staleness.
62
+ 3. If L2 data exists, it is loaded into memory (L1).
63
+ 4. This allows data to survive process restarts or be shared across multiple workers.
64
+
65
+ ### Implementation Protocol
66
+ To implement your own L2 storage, provide a class matching the `StorageBackend` Protocol:
67
+
68
+ ```python
69
+ class MyStorage:
70
+ async def get(self, key: str) -> str | None: ...
71
+ async def set(self, key: str, value: str, ttl: float | None = None) -> None: ...
72
+ async def delete(self, key: str) -> None: ...
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Core Configuration
78
+
79
+ ### QueryClientConfig
80
+
81
+ | Field | Type | Default | Description |
82
+ |-------|------|---------|-------------|
83
+ | `stale_time` | `float` | `0.0` | Seconds before data is considered stale. |
84
+ | `gc_time` | `float` | `300.0` | How long inactive queries persist in L1/L2. |
85
+ | `retry` | `int` | `3` | Number of exponential backoff attempts. |
86
+ | `storage` | `StorageBackend` | `None` | The L2 persistence layer. |
87
+
88
+ ### QueryOptions
89
+
90
+ | Field | Type | Default | Description |
91
+ |-------|------|---------|-------------|
92
+ | `query_key` | `tuple` | **Required** | The hierarchical identifier. |
93
+ | `query_fn` | `coroutine` | **Required** | The async logic to get data. |
94
+ | `refetch_interval` | `float` | `None` | Auto-poll interval (only while observed). |
95
+ | `select` | `callable` | `None` | Transformation applied at the observer level. |
96
+ | `placeholder_data` | `T` | `None` | Instant data to show while first fetch is pending. |