cacheql 0.0.1a0__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.
- cacheql-0.0.1a0/.github/workflows/ci.yml +46 -0
- cacheql-0.0.1a0/.github/workflows/publish.yml +39 -0
- cacheql-0.0.1a0/.gitignore +55 -0
- cacheql-0.0.1a0/PKG-INFO +432 -0
- cacheql-0.0.1a0/README.md +392 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/.gitignore +19 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/Dockerfile +25 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/README.md +210 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/__init__.py +1 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/database.py +274 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/main.py +156 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/resolvers.py +219 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/schema.py +133 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/docker-compose.yml +46 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/requirements.txt +20 -0
- cacheql-0.0.1a0/examples/fastapi-ariadne-redis/test_caching.py +282 -0
- cacheql-0.0.1a0/extras/redis/README.md +3 -0
- cacheql-0.0.1a0/extras/redis/pyproject.toml +36 -0
- cacheql-0.0.1a0/extras/redis/src/cacheql_redis/__init__.py +5 -0
- cacheql-0.0.1a0/extras/redis/src/cacheql_redis/backend.py +158 -0
- cacheql-0.0.1a0/pyproject.toml +64 -0
- cacheql-0.0.1a0/src/cacheql/__init__.py +135 -0
- cacheql-0.0.1a0/src/cacheql/adapters/__init__.py +1 -0
- cacheql-0.0.1a0/src/cacheql/adapters/ariadne/__init__.py +18 -0
- cacheql-0.0.1a0/src/cacheql/adapters/ariadne/decorators.py +236 -0
- cacheql-0.0.1a0/src/cacheql/adapters/ariadne/extension.py +375 -0
- cacheql-0.0.1a0/src/cacheql/adapters/ariadne/graphql.py +55 -0
- cacheql-0.0.1a0/src/cacheql/adapters/ariadne/handler.py +137 -0
- cacheql-0.0.1a0/src/cacheql/adapters/strawberry/__init__.py +5 -0
- cacheql-0.0.1a0/src/cacheql/adapters/strawberry/extension.py +244 -0
- cacheql-0.0.1a0/src/cacheql/core/__init__.py +24 -0
- cacheql-0.0.1a0/src/cacheql/core/entities/__init__.py +23 -0
- cacheql-0.0.1a0/src/cacheql/core/entities/cache_config.py +47 -0
- cacheql-0.0.1a0/src/cacheql/core/entities/cache_control.py +236 -0
- cacheql-0.0.1a0/src/cacheql/core/entities/cache_entry.py +73 -0
- cacheql-0.0.1a0/src/cacheql/core/entities/cache_key.py +69 -0
- cacheql-0.0.1a0/src/cacheql/core/interfaces/__init__.py +13 -0
- cacheql-0.0.1a0/src/cacheql/core/interfaces/cache_backend.py +76 -0
- cacheql-0.0.1a0/src/cacheql/core/interfaces/invalidator.py +45 -0
- cacheql-0.0.1a0/src/cacheql/core/interfaces/key_builder.py +32 -0
- cacheql-0.0.1a0/src/cacheql/core/interfaces/serializer.py +39 -0
- cacheql-0.0.1a0/src/cacheql/core/services/__init__.py +27 -0
- cacheql-0.0.1a0/src/cacheql/core/services/cache_control_calculator.py +259 -0
- cacheql-0.0.1a0/src/cacheql/core/services/cache_service.py +206 -0
- cacheql-0.0.1a0/src/cacheql/core/services/directive_parser.py +245 -0
- cacheql-0.0.1a0/src/cacheql/decorators.py +250 -0
- cacheql-0.0.1a0/src/cacheql/hints.py +200 -0
- cacheql-0.0.1a0/src/cacheql/infrastructure/__init__.py +11 -0
- cacheql-0.0.1a0/src/cacheql/infrastructure/backends/__init__.py +5 -0
- cacheql-0.0.1a0/src/cacheql/infrastructure/backends/memory.py +132 -0
- cacheql-0.0.1a0/src/cacheql/infrastructure/key_builders/__init__.py +5 -0
- cacheql-0.0.1a0/src/cacheql/infrastructure/key_builders/default.py +104 -0
- cacheql-0.0.1a0/src/cacheql/infrastructure/serializers/__init__.py +5 -0
- cacheql-0.0.1a0/src/cacheql/infrastructure/serializers/json.py +83 -0
- cacheql-0.0.1a0/src/cacheql/utils/__init__.py +5 -0
- cacheql-0.0.1a0/src/cacheql/utils/hashing.py +38 -0
- cacheql-0.0.1a0/tests/__init__.py +1 -0
- cacheql-0.0.1a0/tests/conftest.py +19 -0
- cacheql-0.0.1a0/tests/integration/__init__.py +1 -0
- cacheql-0.0.1a0/tests/integration/ariadne/__init__.py +1 -0
- cacheql-0.0.1a0/tests/integration/ariadne/test_extension.py +340 -0
- cacheql-0.0.1a0/tests/integration/strawberry/__init__.py +1 -0
- cacheql-0.0.1a0/tests/integration/strawberry/test_extension.py +122 -0
- cacheql-0.0.1a0/tests/unit/__init__.py +1 -0
- cacheql-0.0.1a0/tests/unit/core/__init__.py +1 -0
- cacheql-0.0.1a0/tests/unit/core/test_cache_control.py +314 -0
- cacheql-0.0.1a0/tests/unit/core/test_cache_service.py +214 -0
- cacheql-0.0.1a0/tests/unit/core/test_entities.py +139 -0
- cacheql-0.0.1a0/tests/unit/infrastructure/__init__.py +1 -0
- cacheql-0.0.1a0/tests/unit/infrastructure/test_key_builder.py +148 -0
- cacheql-0.0.1a0/tests/unit/infrastructure/test_memory_backend.py +125 -0
- cacheql-0.0.1a0/tests/unit/infrastructure/test_serializer.py +124 -0
- cacheql-0.0.1a0/tests/unit/test_decorators.py +208 -0
- cacheql-0.0.1a0/tests/unit/test_hints.py +165 -0
- cacheql-0.0.1a0/uv.lock +629 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
run: uv python install ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync --all-extras
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: uv run pytest --cov=cacheql --cov-report=xml
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: uv run ruff check src/
|
|
33
|
+
|
|
34
|
+
typecheck:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
|
|
39
|
+
- name: Install uv
|
|
40
|
+
uses: astral-sh/setup-uv@v4
|
|
41
|
+
|
|
42
|
+
- name: Install dependencies
|
|
43
|
+
run: uv sync --all-extras
|
|
44
|
+
|
|
45
|
+
- name: Type check
|
|
46
|
+
run: uv run mypy src/cacheql --ignore-missing-imports
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Install uv
|
|
14
|
+
uses: astral-sh/setup-uv@v4
|
|
15
|
+
|
|
16
|
+
- name: Build package
|
|
17
|
+
run: uv build
|
|
18
|
+
|
|
19
|
+
- name: Upload dist
|
|
20
|
+
uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
steps:
|
|
32
|
+
- name: Download dist
|
|
33
|
+
uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
- name: Publish to PyPI
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
*~
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.coverage
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
|
|
42
|
+
# Mypy
|
|
43
|
+
.mypy_cache/
|
|
44
|
+
|
|
45
|
+
# Ruff
|
|
46
|
+
.ruff_cache/
|
|
47
|
+
|
|
48
|
+
# OS
|
|
49
|
+
.DS_Store
|
|
50
|
+
Thumbs.db
|
|
51
|
+
|
|
52
|
+
# Local
|
|
53
|
+
*.local
|
|
54
|
+
.env
|
|
55
|
+
.env.*
|
cacheql-0.0.1a0/PKG-INFO
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cacheql
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: Server-side caching framework for GraphQL APIs
|
|
5
|
+
Project-URL: Homepage, https://github.com/nogueira-raphael/cacheql
|
|
6
|
+
Project-URL: Repository, https://github.com/nogueira-raphael/cacheql
|
|
7
|
+
Project-URL: Issues, https://github.com/nogueira-raphael/cacheql/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/nogueira-raphael/cacheql/releases
|
|
9
|
+
Author-email: Raphael Nogueira <raphael0608@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: apollo,ariadne,async,cache,cachecontrol,graphql,strawberry
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: cachetools>=5.0
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: ariadne>=0.20; extra == 'all'
|
|
25
|
+
Requires-Dist: redis>=5.0; extra == 'all'
|
|
26
|
+
Requires-Dist: strawberry-graphql>=0.200; extra == 'all'
|
|
27
|
+
Provides-Extra: ariadne
|
|
28
|
+
Requires-Dist: ariadne>=0.20; extra == 'ariadne'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
35
|
+
Provides-Extra: redis
|
|
36
|
+
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
37
|
+
Provides-Extra: strawberry
|
|
38
|
+
Requires-Dist: strawberry-graphql>=0.200; extra == 'strawberry'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# cacheql
|
|
42
|
+
|
|
43
|
+
Server-side caching framework for GraphQL APIs in Python.
|
|
44
|
+
|
|
45
|
+
**Compatible with Apollo Server's `@cacheControl` directive semantics.**
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Apollo-style Cache Control**: Full support for `@cacheControl` directives
|
|
50
|
+
- **Query-level caching**: Cache entire GraphQL query responses
|
|
51
|
+
- **Field-level caching**: Fine-grained cache control per field and type
|
|
52
|
+
- **Dynamic cache hints**: Set cache policies from within resolvers
|
|
53
|
+
- **HTTP Cache-Control headers**: Automatic header generation
|
|
54
|
+
- **Multiple backends**: In-memory (LRU) and Redis support
|
|
55
|
+
- **Framework adapters**: Built-in support for Ariadne and Strawberry
|
|
56
|
+
- **Tag-based invalidation**: Invalidate cache entries by tags
|
|
57
|
+
- **Async-first**: Fully async API for modern Python applications
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Core package with in-memory backend
|
|
63
|
+
pip install cacheql
|
|
64
|
+
|
|
65
|
+
# With Ariadne support
|
|
66
|
+
pip install cacheql[ariadne]
|
|
67
|
+
|
|
68
|
+
# With Strawberry support
|
|
69
|
+
pip install cacheql[strawberry]
|
|
70
|
+
|
|
71
|
+
# With Redis backend
|
|
72
|
+
pip install cacheql[redis]
|
|
73
|
+
|
|
74
|
+
# All optional dependencies
|
|
75
|
+
pip install cacheql[all]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick Start with @cacheControl Directives
|
|
79
|
+
|
|
80
|
+
Following [Apollo Server's caching documentation](https://www.apollographql.com/docs/apollo-server/performance/caching), cacheql supports the `@cacheControl` directive for declarative cache configuration.
|
|
81
|
+
|
|
82
|
+
### Schema Setup
|
|
83
|
+
|
|
84
|
+
```graphql
|
|
85
|
+
# Add the directive definition to your schema
|
|
86
|
+
directive @cacheControl(
|
|
87
|
+
maxAge: Int
|
|
88
|
+
scope: CacheControlScope
|
|
89
|
+
inheritMaxAge: Boolean
|
|
90
|
+
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
|
|
91
|
+
|
|
92
|
+
enum CacheControlScope {
|
|
93
|
+
PUBLIC
|
|
94
|
+
PRIVATE
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Apply directives to types and fields
|
|
98
|
+
type Query {
|
|
99
|
+
# Cache for 5 minutes, shared across all users
|
|
100
|
+
users: [User!]! @cacheControl(maxAge: 300)
|
|
101
|
+
|
|
102
|
+
# Cache for 1 minute, per-user only
|
|
103
|
+
me: User @cacheControl(maxAge: 60, scope: PRIVATE)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type User @cacheControl(maxAge: 600) {
|
|
107
|
+
id: ID!
|
|
108
|
+
name: String!
|
|
109
|
+
# Private data - makes entire response private
|
|
110
|
+
email: String! @cacheControl(scope: PRIVATE)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type Post @cacheControl(maxAge: 300) {
|
|
114
|
+
id: ID!
|
|
115
|
+
title: String!
|
|
116
|
+
# Inherit maxAge from parent (Post's 300s)
|
|
117
|
+
author: User! @cacheControl(inheritMaxAge: true)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Python Setup with Ariadne
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from ariadne import QueryType, make_executable_schema
|
|
125
|
+
from ariadne.asgi import GraphQL
|
|
126
|
+
from cacheql import (
|
|
127
|
+
CacheService,
|
|
128
|
+
CacheConfig,
|
|
129
|
+
InMemoryCacheBackend,
|
|
130
|
+
DefaultKeyBuilder,
|
|
131
|
+
JsonSerializer,
|
|
132
|
+
get_cache_control_directive_sdl,
|
|
133
|
+
)
|
|
134
|
+
from cacheql.adapters.ariadne import CacheExtension
|
|
135
|
+
|
|
136
|
+
# Include directive definition in your schema
|
|
137
|
+
type_defs = get_cache_control_directive_sdl() + """
|
|
138
|
+
type Query {
|
|
139
|
+
users: [User!]! @cacheControl(maxAge: 300)
|
|
140
|
+
me: User @cacheControl(maxAge: 60, scope: PRIVATE)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type User @cacheControl(maxAge: 600) {
|
|
144
|
+
id: ID!
|
|
145
|
+
name: String!
|
|
146
|
+
email: String! @cacheControl(scope: PRIVATE)
|
|
147
|
+
}
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
query = QueryType()
|
|
151
|
+
|
|
152
|
+
@query.field("users")
|
|
153
|
+
async def resolve_users(*_):
|
|
154
|
+
return [{"id": "1", "name": "Alice", "email": "alice@example.com"}]
|
|
155
|
+
|
|
156
|
+
@query.field("me")
|
|
157
|
+
async def resolve_me(*_):
|
|
158
|
+
return {"id": "1", "name": "Alice", "email": "alice@example.com"}
|
|
159
|
+
|
|
160
|
+
schema = make_executable_schema(type_defs, query)
|
|
161
|
+
|
|
162
|
+
# Create cache service
|
|
163
|
+
config = CacheConfig(
|
|
164
|
+
use_cache_control=True,
|
|
165
|
+
default_max_age=0, # No cache by default (conservative)
|
|
166
|
+
)
|
|
167
|
+
cache_service = CacheService(
|
|
168
|
+
backend=InMemoryCacheBackend(maxsize=1000),
|
|
169
|
+
key_builder=DefaultKeyBuilder(),
|
|
170
|
+
serializer=JsonSerializer(),
|
|
171
|
+
config=config,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Create extension with schema for directive parsing
|
|
175
|
+
extension = CacheExtension(cache_service, schema=schema)
|
|
176
|
+
|
|
177
|
+
app = GraphQL(
|
|
178
|
+
schema,
|
|
179
|
+
extensions=[extension],
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Cache Control Semantics
|
|
184
|
+
|
|
185
|
+
Following Apollo Server's rules:
|
|
186
|
+
|
|
187
|
+
### Response Policy Calculation
|
|
188
|
+
|
|
189
|
+
The overall cache policy is determined by the **most restrictive** values:
|
|
190
|
+
|
|
191
|
+
- **maxAge**: Uses the **lowest** value across all fields
|
|
192
|
+
- **scope**: Uses **PRIVATE** if any field specifies PRIVATE
|
|
193
|
+
|
|
194
|
+
### Default Behavior
|
|
195
|
+
|
|
196
|
+
- Root fields (Query, Mutation): Default `maxAge: 0` (no caching)
|
|
197
|
+
- Object/Interface/Union fields: Default `maxAge: 0`
|
|
198
|
+
- Scalar fields: Inherit from parent
|
|
199
|
+
|
|
200
|
+
This conservative approach ensures only explicitly cacheable data gets cached.
|
|
201
|
+
|
|
202
|
+
### HTTP Headers
|
|
203
|
+
|
|
204
|
+
cacheql automatically generates `Cache-Control` headers:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
Cache-Control: max-age=300, public
|
|
208
|
+
Cache-Control: max-age=60, private
|
|
209
|
+
Cache-Control: no-store (when maxAge is 0)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Dynamic Cache Hints in Resolvers
|
|
213
|
+
|
|
214
|
+
Set cache hints dynamically based on runtime conditions:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from cacheql.hints import set_cache_hint, private_cache, no_cache
|
|
218
|
+
|
|
219
|
+
@query.field("user")
|
|
220
|
+
async def resolve_user(_, info, id: str):
|
|
221
|
+
user = await get_user(id)
|
|
222
|
+
|
|
223
|
+
# Set cache hint based on user data
|
|
224
|
+
if user.is_public_profile:
|
|
225
|
+
set_cache_hint(info, max_age=3600, scope="PUBLIC")
|
|
226
|
+
else:
|
|
227
|
+
set_cache_hint(info, max_age=60, scope="PRIVATE")
|
|
228
|
+
|
|
229
|
+
return user
|
|
230
|
+
|
|
231
|
+
@query.field("sensitive_data")
|
|
232
|
+
async def resolve_sensitive(_, info):
|
|
233
|
+
# Disable caching entirely
|
|
234
|
+
no_cache(info)
|
|
235
|
+
return get_sensitive_data()
|
|
236
|
+
|
|
237
|
+
@query.field("my_profile")
|
|
238
|
+
async def resolve_my_profile(_, info):
|
|
239
|
+
# Shorthand for private cache
|
|
240
|
+
private_cache(info, max_age=300)
|
|
241
|
+
return get_current_user_profile(info)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Legacy Mode (Simple TTL-based Caching)
|
|
245
|
+
|
|
246
|
+
For simpler use cases without directive parsing:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from datetime import timedelta
|
|
250
|
+
from cacheql import CacheConfig
|
|
251
|
+
|
|
252
|
+
config = CacheConfig(
|
|
253
|
+
use_cache_control=False, # Disable directive parsing
|
|
254
|
+
default_ttl=timedelta(minutes=5),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# All queries are cached with the default TTL
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Field-Level Caching with Decorators
|
|
261
|
+
|
|
262
|
+
For fine-grained control without schema directives:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from cacheql import cached, invalidates, configure
|
|
266
|
+
|
|
267
|
+
configure(cache_service)
|
|
268
|
+
|
|
269
|
+
@cached(ttl=timedelta(minutes=10), tags=["User", "User:{id}"])
|
|
270
|
+
async def get_user(id: str) -> dict:
|
|
271
|
+
return await db.get_user(id)
|
|
272
|
+
|
|
273
|
+
@invalidates(tags=["User", "User:{id}"])
|
|
274
|
+
async def update_user(id: str, data: dict) -> dict:
|
|
275
|
+
return await db.update_user(id, data)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Redis Backend
|
|
279
|
+
|
|
280
|
+
For distributed deployments:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from cacheql_redis import RedisCacheBackend
|
|
284
|
+
|
|
285
|
+
backend = RedisCacheBackend(
|
|
286
|
+
redis_url="redis://localhost:6379",
|
|
287
|
+
key_prefix="myapp",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
cache_service = CacheService(
|
|
291
|
+
backend=backend,
|
|
292
|
+
key_builder=DefaultKeyBuilder(),
|
|
293
|
+
serializer=JsonSerializer(),
|
|
294
|
+
config=config,
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Configuration
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from datetime import timedelta
|
|
302
|
+
from cacheql import CacheConfig
|
|
303
|
+
|
|
304
|
+
config = CacheConfig(
|
|
305
|
+
enabled=True, # Enable/disable caching
|
|
306
|
+
default_ttl=timedelta(minutes=5), # Default TTL (legacy mode)
|
|
307
|
+
max_size=1000, # Max entries for LRU backends
|
|
308
|
+
key_prefix="cacheql", # Prefix for cache keys
|
|
309
|
+
|
|
310
|
+
# Cache control settings (Apollo-style)
|
|
311
|
+
use_cache_control=True, # Enable directive parsing
|
|
312
|
+
default_max_age=0, # Default maxAge in seconds
|
|
313
|
+
calculate_http_headers=True, # Generate Cache-Control headers
|
|
314
|
+
|
|
315
|
+
# Query behavior
|
|
316
|
+
cache_queries=True, # Cache query responses
|
|
317
|
+
cache_mutations=False, # Don't cache mutations
|
|
318
|
+
auto_invalidate_on_mutation=True, # Auto-invalidate on mutations
|
|
319
|
+
)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Accessing Cache Policy
|
|
323
|
+
|
|
324
|
+
After request execution, you can access the calculated cache policy:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
extension = CacheExtension(cache_service, schema=schema)
|
|
328
|
+
|
|
329
|
+
# After request_finished is called
|
|
330
|
+
policy = extension.get_cache_policy()
|
|
331
|
+
if policy:
|
|
332
|
+
print(f"maxAge: {policy.max_age}")
|
|
333
|
+
print(f"scope: {policy.scope.value}")
|
|
334
|
+
print(f"cacheable: {policy.is_cacheable}")
|
|
335
|
+
print(f"header: {policy.to_http_header()}")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Cache Invalidation
|
|
339
|
+
|
|
340
|
+
### By Tags
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
await cache_service.invalidate(["User"])
|
|
344
|
+
await cache_service.invalidate(["User:123"])
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Clear All
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
await cache_service.clear()
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Statistics
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
stats = cache_service.stats
|
|
357
|
+
print(f"Hits: {stats['hits']}")
|
|
358
|
+
print(f"Misses: {stats['misses']}")
|
|
359
|
+
print(f"Total: {stats['total']}")
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Response Extensions
|
|
363
|
+
|
|
364
|
+
cacheql adds metadata to GraphQL response extensions:
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"data": { ... },
|
|
369
|
+
"extensions": {
|
|
370
|
+
"cacheql": {
|
|
371
|
+
"cached": false,
|
|
372
|
+
"cacheControl": {
|
|
373
|
+
"maxAge": 300,
|
|
374
|
+
"scope": "PUBLIC",
|
|
375
|
+
"cacheable": true
|
|
376
|
+
},
|
|
377
|
+
"stats": {
|
|
378
|
+
"hits": 10,
|
|
379
|
+
"misses": 5,
|
|
380
|
+
"total": 15
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Architecture
|
|
388
|
+
|
|
389
|
+
cacheql follows Domain-Driven Design principles:
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
┌─────────────────────────────────────────────┐
|
|
393
|
+
│ Adapters (Ariadne/Strawberry) │
|
|
394
|
+
├─────────────────────────────────────────────┤
|
|
395
|
+
│ Application Services │
|
|
396
|
+
├─────────────────────────────────────────────┤
|
|
397
|
+
│ Domain (Core) │
|
|
398
|
+
├─────────────────────────────────────────────┤
|
|
399
|
+
│ Infrastructure │
|
|
400
|
+
└─────────────────────────────────────────────┘
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Core Components
|
|
404
|
+
|
|
405
|
+
- `CacheHint`: Represents cache control settings (maxAge, scope)
|
|
406
|
+
- `CacheScope`: Enum for PUBLIC/PRIVATE scope
|
|
407
|
+
- `ResponseCachePolicy`: Calculated policy for entire response
|
|
408
|
+
- `CacheControlCalculator`: Calculates policy from hints
|
|
409
|
+
- `DirectiveParser`: Parses @cacheControl from schema
|
|
410
|
+
|
|
411
|
+
## Development
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# Install dev dependencies
|
|
415
|
+
pip install -e ".[dev]"
|
|
416
|
+
|
|
417
|
+
# Run tests
|
|
418
|
+
pytest tests/ -v
|
|
419
|
+
|
|
420
|
+
# Run tests with coverage
|
|
421
|
+
pytest tests/ --cov=cacheql --cov-report=html
|
|
422
|
+
|
|
423
|
+
# Type checking
|
|
424
|
+
mypy src/cacheql
|
|
425
|
+
|
|
426
|
+
# Linting
|
|
427
|
+
ruff check src/cacheql
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## License
|
|
431
|
+
|
|
432
|
+
MIT License - see LICENSE file for details.
|