psycache 26.1.0__tar.gz → 26.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. psycache-26.2.0/.git_archival.txt +3 -0
  2. psycache-26.2.0/.gitattributes +1 -0
  3. {psycache-26.1.0 → psycache-26.2.0}/.github/workflows/ci.yml +22 -0
  4. {psycache-26.1.0 → psycache-26.2.0}/.gitignore +1 -0
  5. psycache-26.2.0/.readthedocs.yaml +26 -0
  6. psycache-26.2.0/CHANGELOG.md +35 -0
  7. psycache-26.2.0/PKG-INFO +109 -0
  8. psycache-26.2.0/README.md +72 -0
  9. {psycache-26.1.0 → psycache-26.2.0}/conftest.py +9 -7
  10. psycache-26.2.0/docs/api-types.md +9 -0
  11. psycache-26.2.0/docs/api.md +58 -0
  12. psycache-26.2.0/docs/assets/javascript/readthedocs.js +8 -0
  13. psycache-26.2.0/docs/async.md +64 -0
  14. psycache-26.2.0/docs/cleanup.md +69 -0
  15. psycache-26.2.0/docs/getting-started.md +86 -0
  16. psycache-26.2.0/docs/index.md +39 -0
  17. psycache-26.2.0/docs/instrumentation/custom.md +40 -0
  18. psycache-26.2.0/docs/instrumentation/index.md +32 -0
  19. psycache-26.2.0/docs/instrumentation/prometheus.md +28 -0
  20. psycache-26.2.0/docs/instrumentation/sentry.md +12 -0
  21. psycache-26.2.0/docs/pool-adapters.md +68 -0
  22. psycache-26.2.0/docs/quality-of-life.md +73 -0
  23. psycache-26.2.0/docs/raw-queries.md +81 -0
  24. {psycache-26.1.0 → psycache-26.2.0}/pyproject.toml +3 -2
  25. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/_async.py +53 -27
  26. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/_sync.py +70 -20
  27. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/_tables.py +6 -0
  28. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/instrumentation/sentry.py +25 -11
  29. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/psycopg_pool.py +13 -6
  30. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/sqlalchemy.py +17 -7
  31. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/typing.py +4 -4
  32. {psycache-26.1.0 → psycache-26.2.0}/tests/test_maintenance.py +1 -1
  33. {psycache-26.1.0 → psycache-26.2.0}/tests/test_raw.py +1 -1
  34. {psycache-26.1.0 → psycache-26.2.0}/tests/test_raw_async.py +2 -2
  35. {psycache-26.1.0 → psycache-26.2.0}/tests/typing/core.py +3 -2
  36. {psycache-26.1.0 → psycache-26.2.0}/tox.ini +17 -2
  37. psycache-26.2.0/zensical.toml +156 -0
  38. psycache-26.1.0/CHANGELOG.md +0 -16
  39. psycache-26.1.0/PKG-INFO +0 -381
  40. psycache-26.1.0/README.md +0 -343
  41. {psycache-26.1.0 → psycache-26.2.0}/.github/AI_POLICY.md +0 -0
  42. {psycache-26.1.0 → psycache-26.2.0}/.github/CODE_OF_CONDUCT.md +0 -0
  43. {psycache-26.1.0 → psycache-26.2.0}/.github/CONTRIBUTING.md +0 -0
  44. {psycache-26.1.0 → psycache-26.2.0}/.github/FUNDING.yml +0 -0
  45. {psycache-26.1.0 → psycache-26.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  46. {psycache-26.1.0 → psycache-26.2.0}/.github/dependabot.yml +0 -0
  47. {psycache-26.1.0 → psycache-26.2.0}/.github/workflows/codeql-analysis.yml +0 -0
  48. {psycache-26.1.0 → psycache-26.2.0}/.github/workflows/pypi-package.yml +0 -0
  49. {psycache-26.1.0 → psycache-26.2.0}/.github/workflows/zizmor.yml +0 -0
  50. {psycache-26.1.0 → psycache-26.2.0}/.pre-commit-config.yaml +0 -0
  51. {psycache-26.1.0 → psycache-26.2.0}/.python-version +0 -0
  52. {psycache-26.1.0 → psycache-26.2.0}/LICENSE +0 -0
  53. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/__init__.py +0 -0
  54. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/__main__.py +0 -0
  55. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/_durations.py +0 -0
  56. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/_sql.py +0 -0
  57. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/instrumentation/__init__.py +0 -0
  58. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/instrumentation/_spans.py +0 -0
  59. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/instrumentation/prometheus.py +0 -0
  60. {psycache-26.1.0 → psycache-26.2.0}/src/psycache/py.typed +0 -0
  61. {psycache-26.1.0 → psycache-26.2.0}/tests/__init__.py +0 -0
  62. {psycache-26.1.0 → psycache-26.2.0}/tests/test_cli.py +0 -0
  63. {psycache-26.1.0 → psycache-26.2.0}/tests/test_psycopg_pool.py +0 -0
  64. {psycache-26.1.0 → psycache-26.2.0}/tests/test_sqlalchemy.py +0 -0
  65. {psycache-26.1.0 → psycache-26.2.0}/tests/typing/README.md +0 -0
  66. {psycache-26.1.0 → psycache-26.2.0}/tests/typing/psycopg_pool.py +0 -0
  67. {psycache-26.1.0 → psycache-26.2.0}/tests/typing/sqlalchemy.py +0 -0
@@ -0,0 +1,3 @@
1
+ node: $Format:%H$
2
+ node-date: $Format:%cI$
3
+ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
@@ -0,0 +1 @@
1
+ .git_archival.txt export-subst
@@ -189,6 +189,27 @@ jobs:
189
189
  uvx --with tox-uv
190
190
  tox run -e docs-doctests
191
191
 
192
+ docs-build:
193
+ name: Build the documentation site
194
+ needs: build-package
195
+ runs-on: ubuntu-latest
196
+ steps:
197
+ - name: Download pre-built packages
198
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
199
+ with:
200
+ name: Packages
201
+ path: dist
202
+ - run: tar xf dist/*.tar.gz --strip-components=1
203
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
204
+ with:
205
+ # Keep in sync with tox.ini's base_python_file.
206
+ python-version-file: .python-version
207
+ - uses: hynek/setup-cached-uv@4300ec2180bc77d705e626a34e381b81a4772c51 # v2.5.0
208
+
209
+ - run: >
210
+ uvx --with tox-uv
211
+ tox run -e docs-build -- --strict
212
+
192
213
  install-dev:
193
214
  name: Verify dev env
194
215
  runs-on: ubuntu-latest
@@ -216,6 +237,7 @@ jobs:
216
237
  - install-dev
217
238
  - typing
218
239
  - docs
240
+ - docs-build
219
241
 
220
242
  runs-on: ubuntu-latest
221
243
 
@@ -10,3 +10,4 @@
10
10
  *.py[co]
11
11
  dist
12
12
  htmlcov
13
+ site
@@ -0,0 +1,26 @@
1
+ ---
2
+ version: 2
3
+
4
+ build:
5
+ os: ubuntu-lts-latest
6
+ tools:
7
+ # Keep in sync with .python-version and tox.ini's docs base_python.
8
+ python: "3.14"
9
+ jobs:
10
+ create_environment:
11
+ # Need the tags to calculate the version.
12
+ - git fetch --tags
13
+
14
+ - asdf plugin add uv
15
+ - asdf install uv latest
16
+ - asdf global uv latest
17
+
18
+ build:
19
+ html:
20
+ # Set canonical URL
21
+ - sed -i "s|https://psycache.hynek.me/|$READTHEDOCS_CANONICAL_URL|g" zensical.toml
22
+ - uvx --with tox-uv tox run -e docs-build
23
+ # Zensical builds into site/; hand it to Read the Docs.
24
+ # https://docs.readthedocs.com/platform/latest/intro/zensical.html
25
+ - mkdir -p $READTHEDOCS_OUTPUT/html/
26
+ - cp --recursive site/* $READTHEDOCS_OUTPUT/html/
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Calendar Versioning](https://calver.org/).
6
+
7
+ The **first number** of the version is the year.
8
+ The **second number** is incremented with each release, starting at 1 for each year.
9
+ The **third number** is for emergencies when we need to start branches for older releases.
10
+
11
+ > [!IMPORTANT]
12
+ > This package is currently in beta and looks forward to your feedback.
13
+ > The code is battle-tested, but APIs may change.
14
+
15
+ <!-- changelog follows -->
16
+
17
+ ## [26.2.0](https://github.com/hynek/psycache/compare/26.1.0...26.2.0) - 2026-06-25
18
+
19
+ ### Added
20
+
21
+ - Proper docs at <https://psycache.hynek.me/>.
22
+ [#2](https://github.com/hynek/psycache/pull/2)
23
+
24
+
25
+ ## Changed
26
+
27
+ - Replaced *attrs* by hand-written classes.
28
+ Sadly, the documentation ecosystem is not ready and *dataclasses* are not fit for public APIs.
29
+ This means *psycopg* is the **only** dependency.
30
+ [#3](https://github.com/hynek/psycache/pull/3)
31
+
32
+
33
+ ## [26.1.0](https://github.com/hynek/psycache/tree/26.1.0) - 2026-06-24
34
+
35
+ Initial release.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: psycache
3
+ Version: 26.2.0
4
+ Summary: A psycopg-Backed PostgreSQL Cache
5
+ Project-URL: Documentation, https://psycache.hynek.me/
6
+ Project-URL: Changelog, https://github.com/hynek/psycache/blob/main/CHANGELOG.md
7
+ Project-URL: GitHub, https://github.com/hynek/psycache
8
+ Project-URL: Funding, https://github.com/sponsors/hynek
9
+ Project-URL: Tidelift, https://tidelift.com?utm_source=lifter&utm_medium=referral&utm_campaign=hynek
10
+ Project-URL: Mastodon, https://mastodon.social/@hynek
11
+ Project-URL: Bluesky, https://bsky.app/profile/hynek.me
12
+ Project-URL: Twitter, https://twitter.com/hynek
13
+ Author-email: Hynek Schlawack <hs@ox.cx>
14
+ License-Expression: MIT
15
+ License-File: LICENSE
16
+ Keywords: cache,postgres,postgresql,psycopg
17
+ Classifier: Development Status :: 4 - Beta
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: psycopg
26
+ Provides-Extra: pool
27
+ Requires-Dist: psycopg-pool; extra == 'pool'
28
+ Provides-Extra: prometheus
29
+ Requires-Dist: prometheus-client; extra == 'prometheus'
30
+ Provides-Extra: sentry
31
+ Requires-Dist: sentry-sdk; extra == 'sentry'
32
+ Provides-Extra: sqlalchemy
33
+ Requires-Dist: sqlalchemy; extra == 'sqlalchemy'
34
+ Provides-Extra: sqlalchemy-asyncio
35
+ Requires-Dist: sqlalchemy[asyncio]; extra == 'sqlalchemy-asyncio'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # *psycache*: *psycopg*-Backed PostgreSQL Cache
39
+
40
+ [![License: MIT](https://img.shields.io/badge/license-MIT-C06524)](https://github.com/hynek/psycache/blob/main/LICENSE)
41
+ [![Documentation](https://img.shields.io/badge/Docs-Read%20the%20Docs-black)](https://psycache.hynek.me/)
42
+ [![PyPI version](https://img.shields.io/pypi/v/psycache)](https://pypi.org/project/psycache/)
43
+ [![No AI slop inside.](https://img.shields.io/badge/no-slop-purple)](https://github.com/hynek/psycache/blob/main/.github/AI_POLICY.md)
44
+
45
+
46
+ A simple key-value cache that stores JSON in PostgreSQL through [*psycopg*](https://www.psycopg.org/) 3, with TTL-based expiration and pluggable instrumentation.
47
+
48
+ - Sync and async ✔︎
49
+ - Type-safe ✔︎
50
+ - Adapters for [SQLAlchemy](https://www.sqlalchemy.org) and [*psycopg-pool*](https://www.psycopg.org/psycopg3/docs/api/pool.html) ✔︎
51
+
52
+ ---
53
+
54
+ *psycache* uses an [unlogged table](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED) for performance and stores values as [JSONB](https://www.postgresql.org/docs/current/datatype-json.html) for versatility.
55
+
56
+ It's a great fit when you already have PostgreSQL and need a fast cache without introducing another piece of infrastructure like Redis.
57
+ For example, you can safely share a SQLAlchemy [`Engine`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Engine) (or [`AsyncEngine`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine)) with *psycache*.
58
+
59
+
60
+ ## Quick Start
61
+
62
+ Let's hitch-hike on a SQLAlchemy engine as a quick example!
63
+
64
+ First, install *psycache* from PyPI with the `sqlalchemy` extra:
65
+
66
+ ```console
67
+ $ uv pip install "psycache[sqlalchemy]"
68
+ ```
69
+
70
+ Initialize the cache table once (`python -Im psycache init-db <dsn>` does the same from the shell), then store and retrieve JSON with a TTL:
71
+
72
+ ```python
73
+ import psycopg
74
+
75
+ from sqlalchemy import create_engine
76
+
77
+ import psycache
78
+
79
+ from psycache import PostgresCache
80
+ from psycache.sqlalchemy import SQLAlchemyCachePool
81
+
82
+ with psycopg.connect(
83
+ "postgresql://psycache@127.0.0.1/psycache", autocommit=True
84
+ ) as conn:
85
+ psycache.init_db(conn)
86
+
87
+ engine = create_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
88
+ cache = PostgresCache(SQLAlchemyCachePool(engine))
89
+
90
+ cache.put_raw("user:alice", {"score": 42}, ttl=300)
91
+ value = cache.get_raw("user:alice")
92
+ # {"score": 42}
93
+
94
+ engine.dispose()
95
+ ```
96
+
97
+
98
+ ## Documentation
99
+
100
+ Full documentation lives at **<https://psycache.hynek.me/>**.
101
+
102
+
103
+ <!-- --8<-- [start:credits] -->
104
+ ## Credits
105
+
106
+ *psycache* is written by [Hynek Schlawack](https://hynek.me/) and distributed under the terms of the [MIT license](https://choosealicense.com/licenses/mit/).
107
+
108
+ The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/) and all my fabulous [GitHub Sponsors](https://github.com/sponsors/hynek).
109
+ <!-- --8<-- [end:credits] -->
@@ -0,0 +1,72 @@
1
+ # *psycache*: *psycopg*-Backed PostgreSQL Cache
2
+
3
+ [![License: MIT](https://img.shields.io/badge/license-MIT-C06524)](https://github.com/hynek/psycache/blob/main/LICENSE)
4
+ [![Documentation](https://img.shields.io/badge/Docs-Read%20the%20Docs-black)](https://psycache.hynek.me/)
5
+ [![PyPI version](https://img.shields.io/pypi/v/psycache)](https://pypi.org/project/psycache/)
6
+ [![No AI slop inside.](https://img.shields.io/badge/no-slop-purple)](https://github.com/hynek/psycache/blob/main/.github/AI_POLICY.md)
7
+
8
+
9
+ A simple key-value cache that stores JSON in PostgreSQL through [*psycopg*](https://www.psycopg.org/) 3, with TTL-based expiration and pluggable instrumentation.
10
+
11
+ - Sync and async ✔︎
12
+ - Type-safe ✔︎
13
+ - Adapters for [SQLAlchemy](https://www.sqlalchemy.org) and [*psycopg-pool*](https://www.psycopg.org/psycopg3/docs/api/pool.html) ✔︎
14
+
15
+ ---
16
+
17
+ *psycache* uses an [unlogged table](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED) for performance and stores values as [JSONB](https://www.postgresql.org/docs/current/datatype-json.html) for versatility.
18
+
19
+ It's a great fit when you already have PostgreSQL and need a fast cache without introducing another piece of infrastructure like Redis.
20
+ For example, you can safely share a SQLAlchemy [`Engine`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Engine) (or [`AsyncEngine`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine)) with *psycache*.
21
+
22
+
23
+ ## Quick Start
24
+
25
+ Let's hitch-hike on a SQLAlchemy engine as a quick example!
26
+
27
+ First, install *psycache* from PyPI with the `sqlalchemy` extra:
28
+
29
+ ```console
30
+ $ uv pip install "psycache[sqlalchemy]"
31
+ ```
32
+
33
+ Initialize the cache table once (`python -Im psycache init-db <dsn>` does the same from the shell), then store and retrieve JSON with a TTL:
34
+
35
+ ```python
36
+ import psycopg
37
+
38
+ from sqlalchemy import create_engine
39
+
40
+ import psycache
41
+
42
+ from psycache import PostgresCache
43
+ from psycache.sqlalchemy import SQLAlchemyCachePool
44
+
45
+ with psycopg.connect(
46
+ "postgresql://psycache@127.0.0.1/psycache", autocommit=True
47
+ ) as conn:
48
+ psycache.init_db(conn)
49
+
50
+ engine = create_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
51
+ cache = PostgresCache(SQLAlchemyCachePool(engine))
52
+
53
+ cache.put_raw("user:alice", {"score": 42}, ttl=300)
54
+ value = cache.get_raw("user:alice")
55
+ # {"score": 42}
56
+
57
+ engine.dispose()
58
+ ```
59
+
60
+
61
+ ## Documentation
62
+
63
+ Full documentation lives at **<https://psycache.hynek.me/>**.
64
+
65
+
66
+ <!-- --8<-- [start:credits] -->
67
+ ## Credits
68
+
69
+ *psycache* is written by [Hynek Schlawack](https://hynek.me/) and distributed under the terms of the [MIT license](https://choosealicense.com/licenses/mit/).
70
+
71
+ The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/) and all my fabulous [GitHub Sponsors](https://github.com/sponsors/hynek).
72
+ <!-- --8<-- [end:credits] -->
@@ -6,14 +6,14 @@ import importlib.util
6
6
 
7
7
  from collections.abc import AsyncIterator, Iterator
8
8
  from contextlib import asynccontextmanager, contextmanager
9
+ from dataclasses import dataclass
9
10
  from doctest import ELLIPSIS
10
11
 
11
- import attrs
12
12
  import psycopg
13
13
  import pytest
14
14
 
15
15
  from sybil import Sybil
16
- from sybil.parsers import myst
16
+ from sybil.parsers import markdown
17
17
 
18
18
  from psycache import AsyncPostgresCache, PostgresCache, init_db
19
19
  from psycache.instrumentation.prometheus import PrometheusInstrumentation
@@ -26,11 +26,13 @@ collect_ignore = [
26
26
  if importlib.util.find_spec(name) is None
27
27
  ]
28
28
 
29
+ # The docs are CommonMark/Material (Zensical), so use the Markdown parsers.
30
+ # Executable examples use plain ```python fences; illustrative snippets are
31
+ # preceded by an HTML comment <!-- skip: next -->.
29
32
  pytest_collect_file = Sybil(
30
33
  parsers=[
31
- myst.DocTestDirectiveParser(optionflags=ELLIPSIS),
32
- myst.PythonCodeBlockParser(doctest_optionflags=ELLIPSIS),
33
- myst.SkipParser(),
34
+ markdown.PythonCodeBlockParser(doctest_optionflags=ELLIPSIS),
35
+ markdown.SkipParser(),
34
36
  ],
35
37
  patterns=["*.md"],
36
38
  fixtures=["psycache_database"],
@@ -77,7 +79,7 @@ _INSTRUMENTATIONS = [
77
79
  ]
78
80
 
79
81
 
80
- @attrs.frozen
82
+ @dataclass
81
83
  class _RawCachePool:
82
84
  """
83
85
  A minimal CachePool that opens a psycopg connection per checkout.
@@ -106,7 +108,7 @@ def _cache(request: pytest.FixtureRequest, db_dsn: str):
106
108
  return PostgresCache(_RawCachePool(db_dsn), instrumentations=request.param)
107
109
 
108
110
 
109
- @attrs.frozen
111
+ @dataclass
110
112
  class _RawAsyncCachePool:
111
113
  """
112
114
  A minimal AsyncCachePool that opens a psycopg connection per checkout.
@@ -0,0 +1,9 @@
1
+ ---
2
+ icon: material/api
3
+ ---
4
+
5
+ # Types & Protocols
6
+
7
+ Most parts of *psycache* can be replaced by implementing the appropriate protocols.
8
+
9
+ ::: psycache.typing
@@ -0,0 +1,58 @@
1
+ ---
2
+ icon: material/api
3
+ ---
4
+
5
+ # API Reference
6
+
7
+ ## Core
8
+
9
+ ::: psycache
10
+ options:
11
+ members:
12
+ - init_db
13
+ - PostgresCache
14
+ - AsyncPostgresCache
15
+ - CleanupService
16
+ - AsyncCleanupService
17
+
18
+
19
+ ## Pool adapters
20
+
21
+ ### *psycopg_pool*
22
+
23
+ ::: psycache.psycopg_pool.PsycopgCachePool
24
+ options:
25
+ show_root_heading: true
26
+ members: false
27
+ heading_level: 4
28
+
29
+ ::: psycache.psycopg_pool.AsyncPsycopgCachePool
30
+ options:
31
+ show_root_heading: true
32
+ members: false
33
+ heading_level: 4
34
+
35
+ ### SQLAlchemy
36
+
37
+ ::: psycache.sqlalchemy.SQLAlchemyCachePool
38
+ options:
39
+ show_root_heading: true
40
+ members: false
41
+
42
+ ::: psycache.sqlalchemy.AsyncSQLAlchemyCachePool
43
+ options:
44
+ show_root_heading: true
45
+ members: false
46
+
47
+
48
+ ## Instrumentation
49
+
50
+ ::: psycache.instrumentation.prometheus.PrometheusInstrumentation
51
+ options:
52
+ show_root_heading: true
53
+ members: false
54
+
55
+ ::: psycache.instrumentation.sentry.SentryInstrumentation
56
+ options:
57
+ show_root_heading: true
58
+ members: false
@@ -0,0 +1,8 @@
1
+ document.addEventListener("DOMContentLoaded", function(event) {
2
+ // Trigger Read the Docs' search addon instead of Zensical default
3
+ document.querySelector(".md-search").addEventListener("click", (e) => {
4
+ e.preventDefault();
5
+ const event = new CustomEvent("readthedocs-search-show");
6
+ document.dispatchEvent(event);
7
+ });
8
+ });
@@ -0,0 +1,64 @@
1
+ # Async
2
+
3
+ *psycache* ships asyncio-native API that mirror the synchronous ones.
4
+
5
+ So, [`AsyncPostgresCache`][psycache.AsyncPostgresCache] exposes `get_raw`, `put_raw`, `remove`, `cleanup_expired`, and `flush` – all async methods with the same signatures as their synchronous counterparts.
6
+ It needs an [`AsyncCachePool`][psycache.typing.AsyncCachePool]: anything with an async `connect()` that yields a [`psycopg.AsyncConnection`][].
7
+
8
+ Two adapters are included, again, mirroring their synchronous counterparts.
9
+
10
+
11
+ ## SQLAlchemy
12
+
13
+ [`AsyncSQLAlchemyCachePool`][psycache.sqlalchemy.AsyncSQLAlchemyCachePool] wraps a SQLAlchemy [`AsyncEngine`][sqlalchemy.ext.asyncio.AsyncEngine] (requires `psycache[sqlalchemy-asyncio]`):
14
+
15
+ ```python
16
+ import asyncio
17
+
18
+ from sqlalchemy.ext.asyncio import create_async_engine
19
+
20
+ from psycache import AsyncPostgresCache
21
+ from psycache.sqlalchemy import AsyncSQLAlchemyCachePool
22
+
23
+
24
+ async def main() -> None:
25
+ engine = create_async_engine(
26
+ "postgresql+psycopg://psycache@127.0.0.1/psycache"
27
+ )
28
+ cache = AsyncPostgresCache(AsyncSQLAlchemyCachePool(engine))
29
+
30
+ await cache.put_raw("my-key", {"user": "alice"}, ttl=300)
31
+ value = await cache.get_raw("my-key")
32
+
33
+ await engine.dispose()
34
+
35
+
36
+ asyncio.run(main())
37
+ ```
38
+
39
+
40
+ ## psycopg-pool
41
+
42
+ [`AsyncPsycopgCachePool`][psycache.psycopg_pool.AsyncPsycopgCachePool] wraps a [`psycopg_pool.AsyncConnectionPool`][psycopg_pool.AsyncConnectionPool] (requires `psycache[pool]`):
43
+
44
+ ```python
45
+ import asyncio
46
+
47
+ from psycopg_pool import AsyncConnectionPool
48
+
49
+ from psycache import AsyncPostgresCache
50
+ from psycache.psycopg_pool import AsyncPsycopgCachePool
51
+
52
+
53
+ async def main() -> None:
54
+ async with AsyncConnectionPool(
55
+ "postgresql://psycache@127.0.0.1/psycache", open=False
56
+ ) as pool:
57
+ cache = AsyncPostgresCache(AsyncPsycopgCachePool(pool))
58
+
59
+ await cache.put_raw("my-key", {"user": "alice"}, ttl=300)
60
+ value = await cache.get_raw("my-key")
61
+
62
+
63
+ asyncio.run(main())
64
+ ```
@@ -0,0 +1,69 @@
1
+ # Background Cleanup
2
+
3
+ *psycache* ignores expired keys when reading, but their rows stick around until something deletes them.
4
+ You can call [`PostgresCache.cleanup_expired()`][psycache.PostgresCache.cleanup_expired] yourself, or let *psycache* do it for you in the background.
5
+
6
+
7
+ ## A cleanup thread
8
+
9
+ For synchronous pools, [`PostgresCache.start_cleanup_thread()`][psycache.PostgresCache.start_cleanup_thread] starts a daemon thread that periodically deletes expired entries.
10
+ Use it as a context manager to stop the thread automatically:
11
+
12
+ ```python
13
+ from sqlalchemy import create_engine
14
+
15
+ from psycache import PostgresCache
16
+ from psycache.sqlalchemy import SQLAlchemyCachePool
17
+
18
+
19
+ engine = create_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
20
+ cache = PostgresCache(SQLAlchemyCachePool(engine))
21
+
22
+
23
+ with cache.start_cleanup_thread(interval=60):
24
+ ... # your application runs here
25
+ ```
26
+
27
+ Or manage its lifecycle manually through the returned [`CleanupService`][psycache.CleanupService]:
28
+
29
+ ```python
30
+ svc = cache.start_cleanup_thread(interval=60)
31
+ try:
32
+ ... # your application runs here
33
+ finally:
34
+ svc.stop()
35
+
36
+ engine.dispose()
37
+ ```
38
+
39
+ ## ... or a cleanup task
40
+
41
+ For async pools, use [`AsyncPostgresCache.start_cleanup_task()`][psycache.AsyncPostgresCache.start_cleanup_task] inside a running event loop.
42
+ It starts an [`asyncio.Task`][] that periodically deletes expired entries and can be used as an async context manager.
43
+
44
+ Otherwise, it mirrors the behavior of [`PostgresCache.start_cleanup_thread()`][psycache.PostgresCache.start_cleanup_thread]:
45
+
46
+ ```python
47
+ import asyncio
48
+
49
+ from sqlalchemy.ext.asyncio import create_async_engine
50
+
51
+ from psycache import AsyncPostgresCache
52
+ from psycache.sqlalchemy import AsyncSQLAlchemyCachePool
53
+
54
+
55
+ aengine = create_async_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
56
+ acache = AsyncPostgresCache(AsyncSQLAlchemyCachePool(aengine))
57
+
58
+ async def main():
59
+ async with acache.start_cleanup_task(interval=60):
60
+ ... # your application runs here
61
+
62
+ svc = acache.start_cleanup_task(interval=60)
63
+ try:
64
+ ... # your application runs here
65
+ finally:
66
+ await svc.stop()
67
+
68
+ asyncio.run(main())
69
+ ```
@@ -0,0 +1,86 @@
1
+ ---
2
+ icon: material/rocket-launch
3
+ ---
4
+
5
+ # Getting Started
6
+
7
+ !!! note
8
+
9
+ This documentation uses [*uv*](https://docs.astral.sh/uv/) for package management and so should you.
10
+ It's trivial, though, to translate the commands to your package manager of choice.
11
+
12
+
13
+ ## Installation
14
+
15
+ *psycache* is published on [PyPI](https://pypi.org/project/psycache/), so install it with your favorite package manager:
16
+
17
+ ```console
18
+ $ uv pip install psycache
19
+ ```
20
+
21
+ The core package depends only on [*psycopg*](https://www.psycopg.org/).
22
+ The pool adapters are optional and each lives behind an extra.
23
+ The examples here use SQLAlchemy, so install the `sqlalchemy` extra too:
24
+
25
+ ```console
26
+ $ uv pip install "psycache[sqlalchemy]"
27
+ ```
28
+
29
+ !!! tip
30
+
31
+ See [Connection Pool Adapters][] for the full list of extras and how to bring your own pool.
32
+
33
+
34
+ ## Initialize the database
35
+
36
+ *psycache* keeps everything in a single [unlogged table](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED).
37
+ Create it once – either from Python using [`init_db()`][psycache.init_db]:
38
+
39
+ ```python
40
+ import psycopg
41
+
42
+ import psycache
43
+
44
+
45
+ with psycopg.connect(
46
+ "postgresql://psycache@127.0.0.1/psycache", autocommit=True
47
+ ) as conn:
48
+ psycache.init_db(conn)
49
+ ```
50
+
51
+ …or from the command line:
52
+
53
+ ```console
54
+ $ python -Im psycache init-db postgresql://psycache@127.0.0.1/psycache
55
+ ```
56
+
57
+ This creates the `psycache` unlogged table and an index on `expires_at`.
58
+ [`init_db()`][psycache.init_db] is idempotent, so running it again leaves existing data untouched.
59
+
60
+
61
+ ## Store and retrieve
62
+
63
+ Assuming you already have a SQLAlchemy [`Engine`][sqlalchemy.engine.Engine] in your application, wrap it in a [`SQLAlchemyCachePool`][psycache.sqlalchemy.SQLAlchemyCachePool] and hand it to [`PostgresCache`][psycache.PostgresCache]:
64
+
65
+ ```python
66
+ from sqlalchemy import create_engine
67
+
68
+ from psycache import PostgresCache
69
+ from psycache.sqlalchemy import SQLAlchemyCachePool
70
+
71
+
72
+ engine = create_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
73
+ cache = PostgresCache(SQLAlchemyCachePool(engine))
74
+
75
+ # Store a value with a TTL of 300 seconds.
76
+ cache.put_raw("user:alice", {"score": 42}, ttl=300)
77
+
78
+ # Retrieve it (returns None if missing or expired).
79
+ value = cache.get_raw("user:alice")
80
+ # {"score": 42}
81
+
82
+ engine.dispose()
83
+ ```
84
+
85
+ That's it!
86
+ You've got a working cache.
@@ -0,0 +1,39 @@
1
+ # psycache
2
+
3
+ *A psycopg-backed PostgreSQL cache.*
4
+
5
+ ---
6
+
7
+ **psycache** is a simple key-value cache that stores JSON in PostgreSQL through [*psycopg*](https://www.psycopg.org/psycopg3/docs/) 3, with TTL-based expiration, and pluggable instrumentation.
8
+
9
+ - **Sync and async**: [`PostgresCache`][psycache.PostgresCache] and a fully async [`AsyncPostgresCache`][psycache.AsyncPostgresCache] that mirrors its API.
10
+
11
+ - **Type-safe**: fully typed, checked using Mypy, *ty*, Pyrefly, and Pyright.
12
+
13
+ - **Bring your own pool**: first-class adapters for [SQLAlchemy](https://www.sqlalchemy.org) and [*psycopg-pool*](https://www.psycopg.org/psycopg3/docs/api/pool.html), or implement a tiny protocol (*one* method!) yourself.
14
+
15
+
16
+ ## Why PostgreSQL?
17
+
18
+ Modern PostgreSQL on modern servers is fast enough for almost everyone's use cases.
19
+
20
+ *psycache* stores values as [JSONB](https://www.postgresql.org/docs/current/datatype-json.html) in an [unlogged table](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED).
21
+ Unlogged tables skip the [write-ahead log](https://www.postgresql.org/docs/current/wal.html) for speed, while JSONB keeps values flexible and queryable.
22
+
23
+ It's a great fit when you **already run PostgreSQL** and want a fast cache without operating another piece of infrastructure like Redis.
24
+ You can even share an existing SQLAlchemy [`Engine`][sqlalchemy.engine.Engine] (or [`AsyncEngine`][sqlalchemy.ext.asyncio.AsyncEngine]) with *psycache*, so the cache rides on the connections you already have.
25
+
26
+
27
+ ## Get started
28
+
29
+ <div class="grid cards" markdown>
30
+
31
+ - :material-rocket-launch: **[Getting Started](getting-started.md)**: install *psycache*, initialize the table, and store your first value.
32
+ - :material-book-open-variant: **[Raw Queries][]**: Our low-level API: `get_raw`, `put_raw`, TTLs, and instrumentation span names.
33
+ - :material-cog: **[Connection Pool Adapters][]**: wire up SQLAlchemy, *psycopg-pool*, or your own pool.
34
+ - :material-beach: **[Quality of Life][]**: embrace the simple life.
35
+
36
+ </div>
37
+
38
+
39
+ --8<-- "README.md:credits"