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.
Files changed (75) hide show
  1. cacheql-0.0.1a0/.github/workflows/ci.yml +46 -0
  2. cacheql-0.0.1a0/.github/workflows/publish.yml +39 -0
  3. cacheql-0.0.1a0/.gitignore +55 -0
  4. cacheql-0.0.1a0/PKG-INFO +432 -0
  5. cacheql-0.0.1a0/README.md +392 -0
  6. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/.gitignore +19 -0
  7. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/Dockerfile +25 -0
  8. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/README.md +210 -0
  9. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/__init__.py +1 -0
  10. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/database.py +274 -0
  11. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/main.py +156 -0
  12. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/resolvers.py +219 -0
  13. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/app/schema.py +133 -0
  14. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/docker-compose.yml +46 -0
  15. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/requirements.txt +20 -0
  16. cacheql-0.0.1a0/examples/fastapi-ariadne-redis/test_caching.py +282 -0
  17. cacheql-0.0.1a0/extras/redis/README.md +3 -0
  18. cacheql-0.0.1a0/extras/redis/pyproject.toml +36 -0
  19. cacheql-0.0.1a0/extras/redis/src/cacheql_redis/__init__.py +5 -0
  20. cacheql-0.0.1a0/extras/redis/src/cacheql_redis/backend.py +158 -0
  21. cacheql-0.0.1a0/pyproject.toml +64 -0
  22. cacheql-0.0.1a0/src/cacheql/__init__.py +135 -0
  23. cacheql-0.0.1a0/src/cacheql/adapters/__init__.py +1 -0
  24. cacheql-0.0.1a0/src/cacheql/adapters/ariadne/__init__.py +18 -0
  25. cacheql-0.0.1a0/src/cacheql/adapters/ariadne/decorators.py +236 -0
  26. cacheql-0.0.1a0/src/cacheql/adapters/ariadne/extension.py +375 -0
  27. cacheql-0.0.1a0/src/cacheql/adapters/ariadne/graphql.py +55 -0
  28. cacheql-0.0.1a0/src/cacheql/adapters/ariadne/handler.py +137 -0
  29. cacheql-0.0.1a0/src/cacheql/adapters/strawberry/__init__.py +5 -0
  30. cacheql-0.0.1a0/src/cacheql/adapters/strawberry/extension.py +244 -0
  31. cacheql-0.0.1a0/src/cacheql/core/__init__.py +24 -0
  32. cacheql-0.0.1a0/src/cacheql/core/entities/__init__.py +23 -0
  33. cacheql-0.0.1a0/src/cacheql/core/entities/cache_config.py +47 -0
  34. cacheql-0.0.1a0/src/cacheql/core/entities/cache_control.py +236 -0
  35. cacheql-0.0.1a0/src/cacheql/core/entities/cache_entry.py +73 -0
  36. cacheql-0.0.1a0/src/cacheql/core/entities/cache_key.py +69 -0
  37. cacheql-0.0.1a0/src/cacheql/core/interfaces/__init__.py +13 -0
  38. cacheql-0.0.1a0/src/cacheql/core/interfaces/cache_backend.py +76 -0
  39. cacheql-0.0.1a0/src/cacheql/core/interfaces/invalidator.py +45 -0
  40. cacheql-0.0.1a0/src/cacheql/core/interfaces/key_builder.py +32 -0
  41. cacheql-0.0.1a0/src/cacheql/core/interfaces/serializer.py +39 -0
  42. cacheql-0.0.1a0/src/cacheql/core/services/__init__.py +27 -0
  43. cacheql-0.0.1a0/src/cacheql/core/services/cache_control_calculator.py +259 -0
  44. cacheql-0.0.1a0/src/cacheql/core/services/cache_service.py +206 -0
  45. cacheql-0.0.1a0/src/cacheql/core/services/directive_parser.py +245 -0
  46. cacheql-0.0.1a0/src/cacheql/decorators.py +250 -0
  47. cacheql-0.0.1a0/src/cacheql/hints.py +200 -0
  48. cacheql-0.0.1a0/src/cacheql/infrastructure/__init__.py +11 -0
  49. cacheql-0.0.1a0/src/cacheql/infrastructure/backends/__init__.py +5 -0
  50. cacheql-0.0.1a0/src/cacheql/infrastructure/backends/memory.py +132 -0
  51. cacheql-0.0.1a0/src/cacheql/infrastructure/key_builders/__init__.py +5 -0
  52. cacheql-0.0.1a0/src/cacheql/infrastructure/key_builders/default.py +104 -0
  53. cacheql-0.0.1a0/src/cacheql/infrastructure/serializers/__init__.py +5 -0
  54. cacheql-0.0.1a0/src/cacheql/infrastructure/serializers/json.py +83 -0
  55. cacheql-0.0.1a0/src/cacheql/utils/__init__.py +5 -0
  56. cacheql-0.0.1a0/src/cacheql/utils/hashing.py +38 -0
  57. cacheql-0.0.1a0/tests/__init__.py +1 -0
  58. cacheql-0.0.1a0/tests/conftest.py +19 -0
  59. cacheql-0.0.1a0/tests/integration/__init__.py +1 -0
  60. cacheql-0.0.1a0/tests/integration/ariadne/__init__.py +1 -0
  61. cacheql-0.0.1a0/tests/integration/ariadne/test_extension.py +340 -0
  62. cacheql-0.0.1a0/tests/integration/strawberry/__init__.py +1 -0
  63. cacheql-0.0.1a0/tests/integration/strawberry/test_extension.py +122 -0
  64. cacheql-0.0.1a0/tests/unit/__init__.py +1 -0
  65. cacheql-0.0.1a0/tests/unit/core/__init__.py +1 -0
  66. cacheql-0.0.1a0/tests/unit/core/test_cache_control.py +314 -0
  67. cacheql-0.0.1a0/tests/unit/core/test_cache_service.py +214 -0
  68. cacheql-0.0.1a0/tests/unit/core/test_entities.py +139 -0
  69. cacheql-0.0.1a0/tests/unit/infrastructure/__init__.py +1 -0
  70. cacheql-0.0.1a0/tests/unit/infrastructure/test_key_builder.py +148 -0
  71. cacheql-0.0.1a0/tests/unit/infrastructure/test_memory_backend.py +125 -0
  72. cacheql-0.0.1a0/tests/unit/infrastructure/test_serializer.py +124 -0
  73. cacheql-0.0.1a0/tests/unit/test_decorators.py +208 -0
  74. cacheql-0.0.1a0/tests/unit/test_hints.py +165 -0
  75. 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.*
@@ -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.