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.
- pystackquery-1.0.1/.github/workflows/publish.yml +55 -0
- pystackquery-1.0.1/.gitignore +203 -0
- pystackquery-1.0.1/.python-version +1 -0
- pystackquery-1.0.1/CONTRIBUTING.md +48 -0
- pystackquery-1.0.1/PKG-INFO +24 -0
- pystackquery-1.0.1/README.md +156 -0
- pystackquery-1.0.1/RELEASING.md +44 -0
- pystackquery-1.0.1/docs/advanced-patterns.md +69 -0
- pystackquery-1.0.1/docs/api-reference.md +96 -0
- pystackquery-1.0.1/docs/cache-management.md +38 -0
- pystackquery-1.0.1/docs/framework-integrations.md +86 -0
- pystackquery-1.0.1/docs/getting-started.md +63 -0
- pystackquery-1.0.1/docs/mutations.md +58 -0
- pystackquery-1.0.1/docs/observers.md +73 -0
- pystackquery-1.0.1/docs/query-options.md +43 -0
- pystackquery-1.0.1/docs/storage-backends.md +127 -0
- pystackquery-1.0.1/examples/__init__.py +1 -0
- pystackquery-1.0.1/examples/background_worker.py +378 -0
- pystackquery-1.0.1/examples/benchmark.py +387 -0
- pystackquery-1.0.1/examples/demo.py +153 -0
- pystackquery-1.0.1/examples/fastapi_app.py +447 -0
- pystackquery-1.0.1/examples/redis_integration.py +122 -0
- pystackquery-1.0.1/examples/tkinter_app.py +442 -0
- pystackquery-1.0.1/pyproject.toml +57 -0
- pystackquery-1.0.1/pystackquery/__init__.py +77 -0
- pystackquery-1.0.1/pystackquery/cache.py +135 -0
- pystackquery-1.0.1/pystackquery/client.py +219 -0
- pystackquery-1.0.1/pystackquery/convenience.py +159 -0
- pystackquery-1.0.1/pystackquery/helpers.py +63 -0
- pystackquery-1.0.1/pystackquery/mutation.py +124 -0
- pystackquery-1.0.1/pystackquery/observer.py +128 -0
- pystackquery-1.0.1/pystackquery/options.py +101 -0
- pystackquery-1.0.1/pystackquery/query.py +339 -0
- pystackquery-1.0.1/pystackquery/state.py +247 -0
- pystackquery-1.0.1/pystackquery/types.py +80 -0
- pystackquery-1.0.1/tests/__init__.py +0 -0
- pystackquery-1.0.1/tests/conftest.py +48 -0
- pystackquery-1.0.1/tests/core/__init__.py +0 -0
- pystackquery-1.0.1/tests/core/test_client.py +98 -0
- pystackquery-1.0.1/tests/core/test_query.py +74 -0
- pystackquery-1.0.1/tests/features/__init__.py +0 -0
- pystackquery-1.0.1/tests/features/test_infrastructure.py +103 -0
- pystackquery-1.0.1/tests/features/test_mutations.py +71 -0
- pystackquery-1.0.1/tests/features/test_observers.py +84 -0
- pystackquery-1.0.1/tests/features/test_persistence.py +90 -0
- 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. |
|