psycache 26.1.0__py3-none-any.whl

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.
psycache/typing.py ADDED
@@ -0,0 +1,154 @@
1
+ # SPDX-FileCopyrightText: 2026 Hynek Schlawack <hs@ox.cx>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ from __future__ import annotations
6
+
7
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager
8
+ from typing import Protocol
9
+
10
+ import psycopg
11
+
12
+
13
+ class CachePool(Protocol):
14
+ """
15
+ Protocol for a class that provides a way to connect to a
16
+ `psycopg.Connection`.
17
+
18
+ Implemented by adapters that wrap a higher-level connection source
19
+ (for example, a psycopg pool or a SQLAlchemy engine) and hand
20
+ out an auto-commit `psycopg.Connection`.
21
+ """
22
+
23
+ def connect(self) -> AbstractContextManager[psycopg.Connection]:
24
+ """
25
+ Connect to the database and return a context manager for an
26
+ auto-commit connection.
27
+ """
28
+ ...
29
+
30
+
31
+ class AsyncCachePool(Protocol):
32
+ """
33
+ Protocol for a class that provides a way to connect to a
34
+ `psycopg.AsyncConnection`.
35
+
36
+ Implemented by adapters that wrap a higher-level connection source
37
+ (for example, a psycopg pool or a SQLAlchemy engine) and hand
38
+ out an auto-commit `psycopg.AsyncConnection`.
39
+ """
40
+
41
+ def connect(
42
+ self,
43
+ ) -> AbstractAsyncContextManager[psycopg.AsyncConnection]:
44
+ """
45
+ Connect to the database and return a context manager for an
46
+ auto-commit connection.
47
+ """
48
+ ...
49
+
50
+
51
+ class CacheInstrumentation(Protocol):
52
+ """
53
+ Cache instrumentation provider.
54
+ """
55
+
56
+ def start_cache_get_span(
57
+ self, key: str, name: str | None
58
+ ) -> AbstractContextManager[CacheGetSpan]:
59
+ """
60
+ Start a span for a cache get operation.
61
+ """
62
+ ...
63
+
64
+ def start_cache_put_span(
65
+ self, key: str, name: str | None
66
+ ) -> AbstractContextManager[CachePutSpan]:
67
+ """
68
+ Start a span for a cache put operation.
69
+ """
70
+ ...
71
+
72
+ def start_cache_remove_span(
73
+ self, key: str
74
+ ) -> AbstractContextManager[CacheRemoveSpan]:
75
+ """
76
+ Start a span for a cache removal operation.
77
+ """
78
+ ...
79
+
80
+ def start_cache_flush_span(
81
+ self,
82
+ ) -> AbstractContextManager[CacheFlushSpan]:
83
+ """
84
+ Start a span for a cache flush operation.
85
+ """
86
+ ...
87
+
88
+ def start_cache_cleanup_span(
89
+ self,
90
+ ) -> AbstractContextManager[CacheCleanupSpan]:
91
+ """
92
+ Start a span for a cache cleanup operation.
93
+ """
94
+ ...
95
+
96
+
97
+ class CacheGetSpan(Protocol):
98
+ """
99
+ Span interface for cache get operations.
100
+ """
101
+
102
+ def record_cache_hit(self, item_size: int) -> None:
103
+ """
104
+ Record a successful cache hit with the item size in bytes.
105
+ """
106
+
107
+ def record_cache_miss(self) -> None:
108
+ """
109
+ Record a cache miss (key not found or expired).
110
+ """
111
+
112
+
113
+ class CachePutSpan(Protocol):
114
+ """
115
+ Span interface for cache write (put) operations.
116
+ """
117
+
118
+ def record_put(self, item_size: int) -> None:
119
+ """
120
+ Record a successful cache write with the item size in bytes.
121
+ """
122
+
123
+
124
+ class CacheFlushSpan(Protocol):
125
+ """
126
+ Span interface for cache flush operations.
127
+ """
128
+
129
+ def record_flush(self, num_flushed: int) -> None:
130
+ """
131
+ Record a successful cache flush with the number of flushed entries.
132
+ """
133
+
134
+
135
+ class CacheCleanupSpan(Protocol):
136
+ """
137
+ Span interface for cache cleanup operations.
138
+ """
139
+
140
+ def record_cleanup(self, num_deleted: int) -> None:
141
+ """
142
+ Record a successful cleanup with the number of deleted entries.
143
+ """
144
+
145
+
146
+ class CacheRemoveSpan(Protocol):
147
+ """
148
+ Span interface for cache remove operations.
149
+ """
150
+
151
+ def record_removed(self) -> None:
152
+ """
153
+ Record a successful cache removal.
154
+ """
@@ -0,0 +1,381 @@
1
+ Metadata-Version: 2.4
2
+ Name: psycache
3
+ Version: 26.1.0
4
+ Summary: A psycopg-Backed PostgreSQL Cache
5
+ Project-URL: Documentation, https://github.com/sponsors/hynek
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: attrs
26
+ Requires-Dist: psycopg
27
+ Provides-Extra: pool
28
+ Requires-Dist: psycopg-pool; extra == 'pool'
29
+ Provides-Extra: prometheus
30
+ Requires-Dist: prometheus-client; extra == 'prometheus'
31
+ Provides-Extra: sentry
32
+ Requires-Dist: sentry-sdk; extra == 'sentry'
33
+ Provides-Extra: sqlalchemy
34
+ Requires-Dist: sqlalchemy; extra == 'sqlalchemy'
35
+ Provides-Extra: sqlalchemy-asyncio
36
+ Requires-Dist: sqlalchemy[asyncio]; extra == 'sqlalchemy-asyncio'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # *psycache*: *psycopg*-Backed PostgreSQL Cache
40
+
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-C06524)](https://github.com/hynek/argon2-cffi-bindings/blob/main/LICENSE)
42
+ [![No AI slop inside.](https://img.shields.io/badge/no-slop-purple)](https://github.com/hynek/argon2-cffi-bindings/blob/main/.github/AI_POLICY.md)
43
+
44
+
45
+ A simple key-value cache that stores JSON in PostgreSQL through [*psycopg*](https://www.psycopg.org/) 3, with TTL-based expiration and pluggable instrumentation.
46
+
47
+ - Sync and async ✔︎
48
+ - Type-safe ✔︎
49
+ - Adapters for [SQLAlchemy](https://www.sqlalchemy.org) and [*psycopg-pool*](https://www.psycopg.org/psycopg3/docs/api/pool.html) ✔︎
50
+
51
+ ---
52
+
53
+ *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.
54
+
55
+ It's a great fit when you already have PostgreSQL and need a fast cache without introducing another infrastructure parts like Redis.
56
+ 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*.
57
+
58
+
59
+ ## Setup
60
+
61
+ Install the `psycache` package from PyPI.
62
+ If you plan to use it with SQLAlchemy like in the following example, install the `sqlalchemy` extra (for example, `uv pip install psycache[sqlalchemy]`).
63
+
64
+ Initialize the database table either programmatically:
65
+
66
+ ```python
67
+ import psycopg, psycache
68
+
69
+ with psycopg.connect("postgresql://psycache@127.0.0.1/psycache", autocommit=True) as conn:
70
+ psycache.init_db(conn)
71
+ ```
72
+
73
+ Or from the command line:
74
+
75
+ ```console
76
+ $ python -m psycache init-db postgresql://psycache@127.0.0.1/psycache
77
+ ```
78
+
79
+ This creates the `psycache` unlogged table and an index on `expires_at`.
80
+
81
+
82
+ ## Basic Usage
83
+
84
+ Assuming you already have a SQLAlchemy `Engine` in your application, you can use `SQLAlchemyCachePool` to adapt it for use with `PostgresCache` and have a very fast cache without any further steps:
85
+
86
+ ```python
87
+ from psycache import PostgresCache
88
+ from psycache.sqlalchemy import SQLAlchemyCachePool
89
+
90
+ from sqlalchemy import create_engine
91
+
92
+ engine = create_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
93
+ cache = PostgresCache(SQLAlchemyCachePool(engine))
94
+
95
+ # Store a value with a TTL of 300 seconds.
96
+ cache.put_raw("user:alice", {"score": 42}, ttl=300)
97
+
98
+ # Retrieve it (returns None if missing or expired).
99
+ value = cache.get_raw("user:alice")
100
+ # {"score": 42}
101
+ ```
102
+
103
+ You can also pass a `datetime.timedelta` to `ttl`:
104
+
105
+ ```python
106
+ import datetime as dt
107
+
108
+ cache.put_raw("other-key", {"data": "value"}, ttl=dt.timedelta(hours=1))
109
+ ```
110
+
111
+ Both `get_raw` and `put_raw` accept an optional *span_name* argument that is used by instrumentation.
112
+ Sentry uses it as the span name and Prometheus adds it as a label.
113
+
114
+ ```python
115
+ cache.put_raw(
116
+ "user:alice", {"name": "alice"}, ttl=300, span_name="store user score"
117
+ )
118
+ value = cache.get_raw("user:alice", span_name="look up user score")
119
+ ```
120
+
121
+ *psycache* ignores expired keys, but you still need ways to delete keys manually:
122
+
123
+ ```python
124
+ # Remove a single key.
125
+ cache.remove("user:alice")
126
+
127
+ # Delete all expired entries.
128
+ num_deleted = cache.cleanup_expired()
129
+
130
+ # Delete everything.
131
+ num_flushed = cache.flush()
132
+
133
+ engine.dispose()
134
+ ```
135
+
136
+
137
+ ### Higher level
138
+
139
+ In practice, you don't want to sling raw dictionaries and remember to add span names.
140
+ So, wrap the cache in your own class to store and retrieve structured data:
141
+
142
+ ```python
143
+ from dataclasses import dataclass
144
+ from typing import Self
145
+
146
+ from psycache import PostgresCache
147
+ from psycache.sqlalchemy import SQLAlchemyCachePool
148
+ from sqlalchemy import Engine
149
+
150
+ @dataclass
151
+ class UserScore:
152
+ name: str
153
+ score: int
154
+
155
+
156
+ class UserCache:
157
+ @classmethod
158
+ def from_engine(cls, engine: Engine, *, ttl: int = 300) -> Self:
159
+ return cls(PostgresCache(SQLAlchemyCachePool(engine)), ttl)
160
+
161
+ def __init__(self, cache: PostgresCache, ttl: int) -> None:
162
+ self._raw_cache = cache
163
+ self._ttl = ttl
164
+
165
+ def look_up_user(self, user_name: str) -> UserScore | None:
166
+ data = self._raw_cache.get_raw(
167
+ f"user:{user_name}",
168
+ span_name="look up user score",
169
+ )
170
+ if data is None:
171
+ return None
172
+
173
+ return UserScore(name=user_name, score=data["score"])
174
+
175
+ def store_user(self, user: UserScore) -> None:
176
+ self._raw_cache.put_raw(
177
+ f"user:{user.name}", {"score": user.score},
178
+ ttl=self._ttl,
179
+ span_name="store user score",
180
+ )
181
+ ```
182
+
183
+ Packages like [*cattrs*](https://cattrs.org/) or [Pydantic](https://docs.pydantic.dev/) can reduce this boilerplate to a single line even for more complex models.
184
+
185
+
186
+ ## Connection Pool
187
+
188
+ `PostgresCache` needs a `CachePool`: anything with a `connect()` method that yields a `psycopg.Connection`.
189
+ The pool adapters are optional and each lives behind an extra; the cache itself needs only `psycopg`.
190
+
191
+ `SQLAlchemyCachePool` wraps a SQLAlchemy `Engine` (requires `psycache[sqlalchemy]`):
192
+
193
+ ```python
194
+ from psycache.sqlalchemy import SQLAlchemyCachePool
195
+
196
+ pool = SQLAlchemyCachePool(engine)
197
+ cache = PostgresCache(pool)
198
+ ```
199
+
200
+ `PsycopgCachePool` wraps a `psycopg_pool.ConnectionPool` (requires `psycache[pool]`):
201
+
202
+ ```python
203
+ from psycopg_pool import ConnectionPool
204
+ from psycache.psycopg_pool import PsycopgCachePool
205
+
206
+
207
+ with ConnectionPool("postgresql://psycache@127.0.0.1/psycache") as pool:
208
+ cache = PostgresCache(PsycopgCachePool(pool))
209
+ ```
210
+
211
+ Or implement the `psycache.typing.CachePool` protocol directly:
212
+
213
+ ```python
214
+ from collections.abc import Iterator
215
+ from contextlib import contextmanager
216
+
217
+ import attrs
218
+
219
+
220
+ @attrs.frozen
221
+ class MyCachePool:
222
+ @contextmanager
223
+ def connect(self) -> Iterator[psycopg.Connection]: ...
224
+ ```
225
+
226
+
227
+ ## Cleanup
228
+
229
+ For sync pools, *psycache* comes with `PostgresCache.start_cleanup_thread()` which starts a daemon thread that periodically deletes expired cache entries.
230
+
231
+ It can be used as a context manager to automatically stop the cleanup thread:
232
+
233
+ ```python
234
+ with cache.start_cleanup_thread(interval=60):
235
+ ...
236
+ ```
237
+
238
+ Or it can be stopped manually via the returned `CleanupService`'s `stop()` method:
239
+
240
+ ```python
241
+ # Or, to manage the lifecycle manually:
242
+ svc = cache.start_cleanup_thread(interval=60)
243
+ try:
244
+ ...
245
+ finally:
246
+ svc.stop()
247
+ ```
248
+
249
+
250
+ ## Async
251
+
252
+ *psycache* also ships an asyncio-native API.
253
+ `AsyncPostgresCache` mirrors `PostgresCache`, but every operation is a coroutine.
254
+ It needs an `AsyncCachePool` (the `psycache.typing.AsyncCachePool` protocol): anything with an async `connect()` that yields a `psycopg.AsyncConnection`.
255
+
256
+ Two adapters are included.
257
+
258
+ `AsyncSQLAlchemyCachePool` (`psycache.sqlalchemy`) wraps a SQLAlchemy `AsyncEngine` (requires `psycache[sqlalchemy-asyncio]`):
259
+
260
+ ```python
261
+ from sqlalchemy.ext.asyncio import create_async_engine
262
+
263
+ from psycache import AsyncPostgresCache
264
+ from psycache.sqlalchemy import AsyncSQLAlchemyCachePool
265
+
266
+
267
+ engine = create_async_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
268
+ cache = AsyncPostgresCache(AsyncSQLAlchemyCachePool(engine))
269
+ ```
270
+
271
+ `AsyncPsycopgCachePool` (`psycache.psycopg_pool`) wraps a psycopg `psycopg_pool.AsyncConnectionPool` (requires `psycache[pool]`):
272
+
273
+ ```python
274
+ import asyncio
275
+
276
+ from psycopg_pool import AsyncConnectionPool
277
+
278
+ from psycache import AsyncPostgresCache
279
+ from psycache.psycopg_pool import AsyncPsycopgCachePool
280
+
281
+
282
+ async def main() -> None:
283
+ async with AsyncConnectionPool(
284
+ "postgresql://psycache@127.0.0.1/psycache"
285
+ ) as pool:
286
+ cache = AsyncPostgresCache(AsyncPsycopgCachePool(pool))
287
+
288
+ await cache.put_raw("my-key", {"user": "alice"}, ttl=300)
289
+ value = await cache.get_raw("my-key")
290
+
291
+
292
+ asyncio.run(main())
293
+ ```
294
+
295
+ ---
296
+
297
+ `AsyncPostgresCache` exposes `get_raw`, `put_raw`, `remove`, `cleanup_expired`, and `flush` – all coroutines with the same signatures as their synchronous counterparts, and it accepts the same `instrumentations`.
298
+
299
+
300
+ ### Async cleanup
301
+
302
+ For async pools, use `AsyncPostgresCache.start_cleanup_task()` inside a running event loop.
303
+
304
+ It starts an `asyncio.Task` that periodically deletes expired cache entries.
305
+ It can be used as an async context manager to automatically stop the cleanup task, or it can be stopped manually via the returned `AsyncCleanupService`'s `stop()` method.
306
+
307
+ ```python
308
+ async def main():
309
+ async with cache.start_cleanup_task(interval=60):
310
+ ...
311
+
312
+ # Or, to manage the lifecycle manually:
313
+ async def main():
314
+ svc = cache.start_cleanup_task(interval=60)
315
+ try:
316
+ ...
317
+ finally:
318
+ await svc.stop()
319
+ ```
320
+
321
+
322
+ ## Instrumentation
323
+
324
+ *psycache* has pluggable instrumentation for observability.
325
+ Pass one or more providers to the `instrumentations` parameter:
326
+
327
+ ```python
328
+ from psycache import PostgresCache
329
+ from psycache.instrumentation.sentry import SentryInstrumentation
330
+ from psycache.instrumentation.prometheus import PrometheusInstrumentation
331
+
332
+ cache = PostgresCache(
333
+ pool,
334
+ instrumentations=(
335
+ SentryInstrumentation(),
336
+ PrometheusInstrumentation(),
337
+ ),
338
+ )
339
+ ```
340
+
341
+
342
+ ### Prometheus
343
+
344
+ `PrometheusInstrumentation` (`psycache.instrumentation.prometheus`) exports the following metrics:
345
+
346
+ | Metric | Type | Labels | Description |
347
+ | --- | --- | --- | --- |
348
+ | `psycache_hits_total` | Counter | `span_name` | Cache hits |
349
+ | `psycache_misses_total` | Counter | `span_name` | Cache misses |
350
+ | `psycache_get_duration_seconds` | Histogram | `span_name` | Get operation latency |
351
+ | `psycache_put_duration_seconds` | Histogram | `span_name` | Put operation latency |
352
+ | `psycache_remove_duration_seconds` | Histogram | | Remove operation latency |
353
+ | `psycache_flush_duration_seconds` | Histogram | | Flush operation latency |
354
+ | `psycache_item_size_bytes` | Histogram | `span_name` | Size of cache items (from `pg_column_size`) |
355
+ | `psycache_flushed_entries` | Histogram | | Entries removed per flush |
356
+ | `psycache_cleanup_last_run_timestamp_seconds` | Gauge | | Timestamp of last cleanup |
357
+ | `psycache_cleanup_deleted_entries` | Gauge | | Entries removed in last cleanup |
358
+
359
+ The `span_name` label is set from the `span_name` argument to `get_raw()` and `put_raw()`. It defaults to `""` when not provided.
360
+
361
+ Requires the `prometheus` extra (`uv pip install psycache[prometheus]`).
362
+
363
+
364
+ ### Sentry
365
+
366
+ `SentryInstrumentation` (`psycache.instrumentation.sentry`) creates [Sentry cache spans](https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation/caches-module/) for `get`, `put`, `remove`, and `flush` operations, recording `cache.hit`, `cache.item_size`, and `cache.key` data.
367
+ The `span_name` argument to `get_raw()` and `put_raw()` is used as the Sentry span name (defaults to `"psycache get"` / `"psycache put"`).
368
+
369
+ Requires the `sentry` extra (`uv pip install psycache[sentry]`).
370
+
371
+
372
+ ### Custom Instrumentation
373
+
374
+ You can write your own provider by implementing the `psycache.typing.CacheInstrumentation` protocol.
375
+
376
+
377
+ ## Credits
378
+
379
+ *psycache* is written by [Hynek Schlawack](https://hynek.me/) and distributed under the terms of the [MIT license](https://choosealicense.com/licenses/mit/).
380
+
381
+ 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).
@@ -0,0 +1,19 @@
1
+ psycache/__init__.py,sha256=bVAN56Llg6y7LLX4Va5Pprl1upjf_VrPsGEDyPQCh-s,426
2
+ psycache/__main__.py,sha256=I4J5GjoV4_6cGMDSPt-K3ZxOFuWgQxmjyFaDfCsclcQ,1383
3
+ psycache/_async.py,sha256=rvLOhQrfDk9Cns70IGkvphKgXuIr3CAFvXCt9ryKLm4,5627
4
+ psycache/_durations.py,sha256=2TZIGgWMps9wK1ULbEXvrZEW_a1TFt26WvGdhppd8DU,1480
5
+ psycache/_sql.py,sha256=WzLsLalOpd7vjF_lNH6Ox7y6O6nmwcfIGPAQQNhEfWo,608
6
+ psycache/_sync.py,sha256=3yVe5g2-2FUmAhMcLqO6RmfvbFB7suUs4139a8YRR9g,5283
7
+ psycache/_tables.py,sha256=TkOY_efWmJI2_K0ngBQAxlc6r_XfulS5a1llVO162FI,489
8
+ psycache/psycopg_pool.py,sha256=pim82iroFlcdBFDPOswpzB4-En3BYr9IFY2PdnsGWFo,1437
9
+ psycache/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ psycache/sqlalchemy.py,sha256=FNh4Pc8UE7zZG0g76ssOpd1WvbOJ03EwF6AHP3zdPgs,1301
11
+ psycache/typing.py,sha256=wpFLyxsA6NwIsaWyKrWwYn1P1m8oAGgFwMasOcL3g4w,3711
12
+ psycache/instrumentation/__init__.py,sha256=QtaKfHlmY-hwxm_Wi2xnRW_qS1-rxDQAWKjTgKjS9d4,199
13
+ psycache/instrumentation/_spans.py,sha256=biX_jGQ_k-yJ_B5GrSIxgVgzuU810IwR17F9PRdwn8s,4237
14
+ psycache/instrumentation/prometheus.py,sha256=FQp0xP1UF_Sb2FuackyiAkGNvBJYNgc8xRpHej4eTR8,4120
15
+ psycache/instrumentation/sentry.py,sha256=FJBoGgZCW7AMovNfxrZukI5C_p0EHe4GRTUSyadC_AQ,3482
16
+ psycache-26.1.0.dist-info/METADATA,sha256=Bb0hSCWomvpGTEgoki86jLDMtl2WTmEx6NPyO8EZXjo,12903
17
+ psycache-26.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
18
+ psycache-26.1.0.dist-info/licenses/LICENSE,sha256=T85snmXoD8X6cIj_-OUZ-bybKCrV-93r-BVaG5MiVjY,1089
19
+ psycache-26.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Hynek Schlawack and the psycache contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.