3tears 0.14.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.
Files changed (74) hide show
  1. 3tears-0.14.0.dist-info/METADATA +260 -0
  2. 3tears-0.14.0.dist-info/RECORD +74 -0
  3. 3tears-0.14.0.dist-info/WHEEL +4 -0
  4. 3tears-0.14.0.dist-info/licenses/LICENSE +21 -0
  5. threetears/core/__init__.py +170 -0
  6. threetears/core/_bridge.py +102 -0
  7. threetears/core/backends/__init__.py +16 -0
  8. threetears/core/backends/nats_proxy.py +1125 -0
  9. threetears/core/backends/protocol.py +186 -0
  10. threetears/core/backends/schema_sql.py +705 -0
  11. threetears/core/backends/sql.py +446 -0
  12. threetears/core/cache/__init__.py +6 -0
  13. threetears/core/cache/base.py +131 -0
  14. threetears/core/cache/duckdb.py +419 -0
  15. threetears/core/cache/kv.py +436 -0
  16. threetears/core/cache/sqlite.py +578 -0
  17. threetears/core/collections/__init__.py +58 -0
  18. threetears/core/collections/asyncpg_init.py +90 -0
  19. threetears/core/collections/base.py +1169 -0
  20. threetears/core/collections/durable_store.py +92 -0
  21. threetears/core/collections/flush.py +460 -0
  22. threetears/core/collections/merge.py +79 -0
  23. threetears/core/collections/registry.py +364 -0
  24. threetears/core/collections/schema_backed.py +1575 -0
  25. threetears/core/config.py +38 -0
  26. threetears/core/coordination/__init__.py +29 -0
  27. threetears/core/coordination/lease.py +563 -0
  28. threetears/core/coordination/replay_guard.py +125 -0
  29. threetears/core/data/__init__.py +21 -0
  30. threetears/core/data/collection_factory.py +369 -0
  31. threetears/core/data/migrations/README.md +191 -0
  32. threetears/core/data/migrations/__init__.py +83 -0
  33. threetears/core/data/migrations/drift.py +513 -0
  34. threetears/core/data/migrations/enforcement.py +850 -0
  35. threetears/core/data/migrations/errors.py +68 -0
  36. threetears/core/data/migrations/helpers.py +888 -0
  37. threetears/core/data/migrations/preview.py +154 -0
  38. threetears/core/data/migrations/registry.py +200 -0
  39. threetears/core/data/migrations/runner.py +686 -0
  40. threetears/core/data/migrations/scope.py +33 -0
  41. threetears/core/data/migrations/template.py +135 -0
  42. threetears/core/data/schema.py +327 -0
  43. threetears/core/data/sql_builder.py +127 -0
  44. threetears/core/data/store.py +167 -0
  45. threetears/core/entities/__init__.py +3 -0
  46. threetears/core/entities/base.py +243 -0
  47. threetears/core/exceptions.py +29 -0
  48. threetears/core/models.py +111 -0
  49. threetears/core/namespaces.py +168 -0
  50. threetears/core/pagination.py +188 -0
  51. threetears/core/py.typed +0 -0
  52. threetears/core/security/__init__.py +94 -0
  53. threetears/core/security/encryption.py +97 -0
  54. threetears/core/security/identity_token.py +328 -0
  55. threetears/core/security/jwks_provider.py +239 -0
  56. threetears/core/security/pop.py +174 -0
  57. threetears/core/security/proxy_assertion.py +196 -0
  58. threetears/core/security/proxy_signer.py +131 -0
  59. threetears/core/security/sandbox.py +436 -0
  60. threetears/core/security/secret_refs.py +226 -0
  61. threetears/core/serialization.py +234 -0
  62. threetears/core/testing/__init__.py +57 -0
  63. threetears/core/testing/containers.py +157 -0
  64. threetears/core/testing/fixtures.py +149 -0
  65. threetears/core/testing/sqla_parity.py +339 -0
  66. threetears/core/utils/__init__.py +29 -0
  67. threetears/core/utils/atomic_write.py +85 -0
  68. threetears/core/utils/pg_pool_kwargs.py +366 -0
  69. threetears/knowledge/__init__.py +78 -0
  70. threetears/knowledge/chains.py +215 -0
  71. threetears/knowledge/concept_merge.py +277 -0
  72. threetears/knowledge/events.py +224 -0
  73. threetears/knowledge/merge.py +331 -0
  74. threetears/knowledge/scope.py +88 -0
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.4
2
+ Name: 3tears
3
+ Version: 0.14.0
4
+ Summary: Three-tier caching framework for Python -- L1 SQLite, L2 NATS KV, L3 PostgreSQL
5
+ Project-URL: Repository, https://github.com/pacepace/3tears
6
+ Author: pace
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Database
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.14
18
+ Requires-Dist: 3tears-nats
19
+ Requires-Dist: 3tears-observe
20
+ Requires-Dist: aiosqlite
21
+ Requires-Dist: asyncpg
22
+ Requires-Dist: cryptography>=42
23
+ Requires-Dist: nats-py
24
+ Requires-Dist: pydantic
25
+ Requires-Dist: pyjwt[crypto]>=2.8
26
+ Requires-Dist: sqlalchemy
27
+ Provides-Extra: duckdb
28
+ Requires-Dist: duckdb>=1.0; extra == 'duckdb'
29
+ Provides-Extra: testing
30
+ Requires-Dist: docker>=7.0; extra == 'testing'
31
+ Requires-Dist: pytest>=8.0; extra == 'testing'
32
+ Requires-Dist: testcontainers[nats,postgres]>=4.0; extra == 'testing'
33
+ Provides-Extra: tracing
34
+ Requires-Dist: opentelemetry-api>=1.20; extra == 'tracing'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # 3tears Core
38
+
39
+ Three-tier caching library for Python applications. Provides collections (L1 SQLite -> L2 NATS KV -> L3 PostgreSQL) with subscript access, entity proxy objects, and configurable flush strategies.
40
+
41
+ ## Architecture
42
+
43
+ ```
44
+ L1 (SQLite, in-process, sync) -> L2 (NATS KV, shared, async) -> L3 (PostgreSQL, persistent, async)
45
+ ```
46
+
47
+ - **L1**: In-memory SQLite via WAL mode. Sync access. Used by entity attribute reads and writes.
48
+ - **L2**: NATS KV shared cache. Async. Cross-pod consistency for multi-instance deployments.
49
+ - **L3**: PostgreSQL (or PostGIS, YugabyteDB, etc.). Async. Source of truth.
50
+
51
+ Reads promote up the stack (L3 miss -> L2 miss -> L1 hit on next access). Writes flow down (L1 -> L2 -> L3, with optional deferred flush).
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. Configure the Registry
56
+
57
+ ```python
58
+ from threetears.core.collections.registry import CollectionRegistry
59
+ from threetears.core.cache.sqlite import SQLiteBackend
60
+
61
+ # Create and configure
62
+ l1 = SQLiteBackend("my_app_cache")
63
+ l1.initialize(sa_metadata) # SQLAlchemy metadata with your table definitions
64
+
65
+ registry = CollectionRegistry()
66
+ registry.configure(
67
+ l1_backend=l1, # SQLiteBackend instance
68
+ l2_client=nats_client, # NATS client (optional, None to skip L2)
69
+ l3_pool=postgres_pool, # asyncpg pool
70
+ )
71
+ ```
72
+
73
+ ### 2. Per-Collection Pool Overrides
74
+
75
+ Different collections can use different databases:
76
+
77
+ ```python
78
+ # Default: all collections use YugabyteDB
79
+ registry.configure(l3_pool=yugabyte_pool)
80
+
81
+ # Override: geo collection uses PostGIS
82
+ registry.configure() # keep defaults
83
+ # When creating the collection, register with override:
84
+ geo_collection = GeoCollection(registry, config, nats_client, write_buffer)
85
+ registry.register(geo_collection, l3_pool=postgis_pool)
86
+ ```
87
+
88
+ ### 3. Define a Collection
89
+
90
+ ```python
91
+ from threetears.core.collections.base import BaseCollection
92
+ from threetears.core.entities.base import BaseEntity
93
+
94
+ class UserEntity(BaseEntity):
95
+ primary_key_field = "user_id"
96
+
97
+ class UsersCollection(BaseCollection[UserEntity]):
98
+ primary_key_column = "user_id"
99
+
100
+ @property
101
+ def table_name(self) -> str:
102
+ return "users"
103
+
104
+ @property
105
+ def entity_class(self) -> type[UserEntity]:
106
+ return UserEntity
107
+
108
+ async def fetch_from_store(self, entity_id):
109
+ row = await self.l3_pool.fetchrow(
110
+ "SELECT * FROM users WHERE user_id = $1", entity_id
111
+ )
112
+ return dict(row) if row else None
113
+
114
+ async def save_to_store(self, data, original_timestamp=None):
115
+ # INSERT or UPDATE with optimistic locking
116
+ ...
117
+
118
+ async def delete_from_store(self, entity_id):
119
+ await self.l3_pool.execute(
120
+ "DELETE FROM users WHERE user_id = $1", entity_id
121
+ )
122
+
123
+ def serialize(self, data):
124
+ return json.dumps(data, default=str).encode()
125
+
126
+ def deserialize(self, data):
127
+ return json.loads(data)
128
+ ```
129
+
130
+ ### 4. Create Collection Instances
131
+
132
+ ```python
133
+ from threetears.core.collections.flush import WriteBuffer
134
+
135
+ write_buffer = WriteBuffer()
136
+ users = UsersCollection(registry, config, nats_client, write_buffer)
137
+ ```
138
+
139
+ The `config` parameter must satisfy the `CoreConfig` protocol:
140
+
141
+ ```python
142
+ class CoreConfig(Protocol):
143
+ collection_flush: str # "ALWAYS", "ON_CHECKPOINT", "ON_SCHEDULE", "ON_SHUTDOWN"
144
+ collection_flush_interval: int # seconds between scheduled flushes
145
+ collection_flush_tables: str # comma-separated table names eligible for deferred flush
146
+ ```
147
+
148
+ ## Access Patterns
149
+
150
+ ### Subscript Access (sync, transparent pull-through)
151
+
152
+ Subscript access is the primary API. On L1 miss, data is transparently pulled through L2/L3 via a background event loop. No `await` needed, no `ensure()` required:
153
+
154
+ ```python
155
+ # Read entity -- pulls through L2/L3 automatically on L1 miss
156
+ entity = users[user_id]
157
+
158
+ # Read single field
159
+ name = users[user_id, "name_display"]
160
+
161
+ # Write single field (writes to L1, tracks for flush)
162
+ users[user_id, "name_display"] = "New Name"
163
+
164
+ # Write full entity data (writes dict to L1)
165
+ users[user_id] = {"user_id": user_id, "name_display": "New Name", ...}
166
+
167
+ # Check if entity is in L1 (does NOT pull through -- L1 only)
168
+ if user_id in users:
169
+ entity = users[user_id]
170
+ ```
171
+
172
+ `__getitem__` raises `KeyError` only if the entity doesn't exist in any tier. The L1 fast path is ~microseconds; an L1 miss with pull-through adds ~50-200us bridge overhead plus the actual L2/L3 I/O time.
173
+
174
+
175
+ For hot-path code where you want to avoid the sync-async bridge overhead on first access, you can pre-warm L1:
176
+
177
+ ```python
178
+ await users.ensure(user_id) # async: pre-warms L1
179
+ entity = users[user_id] # guaranteed L1 hit, no bridge needed
180
+ ```
181
+
182
+ ### Async Operations
183
+
184
+ ```python
185
+ # Three-tier read: L1 -> L2 -> L3, promotes on miss. Returns None if not found.
186
+ entity = await users.get(user_id)
187
+
188
+ # Create a new entity (not persisted until save)
189
+ entity = users.create({"user_id": uuid7(), "name_display": "Alice", ...})
190
+
191
+ # Save through three-tier write path (L3 -> L1 -> L2)
192
+ await users.save_entity(entity)
193
+ # Or via entity directly:
194
+ await entity.save()
195
+
196
+ # Reload from L3 (discards local changes)
197
+ await entity.reload()
198
+
199
+ # Delete from all tiers
200
+ await users.delete(user_id)
201
+
202
+ # Invalidate L1 + L2 (force next read to hit L3)
203
+ await users.invalidate_cache(user_id)
204
+ ```
205
+
206
+ ### Entity Attribute Access
207
+
208
+ Entities are thin cache proxies. Field data lives in L1, not in the entity object.
209
+
210
+ ```python
211
+ entity = await users.get(user_id)
212
+
213
+ # Read (checks entity._changes first, then L1 cache)
214
+ print(entity.name_display)
215
+
216
+ # Write (writes to L1 + tracks change)
217
+ entity.name_display = "Updated Name"
218
+
219
+ # Check dirty state
220
+ entity.is_dirty # True after modification
221
+ entity.is_new # True if created via collection.create()
222
+
223
+ # Get all changes
224
+ entity.get_changes() # {"name_display": "Updated Name"}
225
+
226
+ # Export full entity data from L1
227
+ entity.to_dict()
228
+
229
+ # Persist
230
+ await entity.save()
231
+ ```
232
+
233
+ ## Flush Strategies
234
+
235
+ Controls when deferred writes reach L3 (PostgreSQL):
236
+
237
+ | Strategy | Behavior |
238
+ |---|---|
239
+ | `ALWAYS` | Every `save_entity()` writes to L3 immediately |
240
+ | `ON_CHECKPOINT` | Writes buffer to L1 + L2; flushes to L3 on explicit `flush_pending()` call |
241
+ | `ON_SCHEDULE` | Same as ON_CHECKPOINT but with timer-based auto-flush |
242
+ | `ON_SHUTDOWN` | Writes buffer; flushes to L3 on application shutdown |
243
+
244
+ Only tables listed in `collection_flush_tables` are eligible for deferred writes. All other tables always write immediately regardless of strategy.
245
+
246
+ ## Optimistic Locking
247
+
248
+ Collections use `date_updated` for optimistic locking. When saving an existing entity, the `save_to_store` implementation should check:
249
+
250
+ ```sql
251
+ UPDATE users SET ... WHERE user_id = $1 AND date_updated = $2
252
+ ```
253
+
254
+ If `rows_affected == 0` for an UPDATE, `BaseCollection.save_entity()` raises `ConcurrentModificationError`.
255
+
256
+ ## Subclassing Guide
257
+
258
+ **BaseEntity**: Set `primary_key_field` to your PK column name. Add computed properties as needed. Do NOT store data in instance attributes. All data lives in L1.
259
+
260
+ **BaseCollection**: Set `primary_key_column`. Implement the 5 abstract methods: `fetch_from_store`, `save_to_store`, `delete_from_store`, `serialize`, `deserialize`. Use `self.l3_pool` for database access. Add domain-specific query methods (e.g., `find_by_email`).
@@ -0,0 +1,74 @@
1
+ threetears/core/__init__.py,sha256=mb496zXGod8nu5U5MYqAIimf17RhKGn8HQCwhSZJ35A,7920
2
+ threetears/core/_bridge.py,sha256=ZWQ8VTCoCo3a2qFDGXvMdBkQEtRwKmN4YHM71jVRD-M,3196
3
+ threetears/core/config.py,sha256=F4UQ2xOhPpCnyWtKwcPN0jjLzRdI6wimAduG2EDNoMg,1145
4
+ threetears/core/exceptions.py,sha256=Gb1dX9nHeZwYR8IbSKEWkq64XQmQa5XAUCTdxaVV5uQ,843
5
+ threetears/core/models.py,sha256=Qct7JQ745FBMhHIHkys1B50_taOIQS9v9R3x68Y1_XE,3315
6
+ threetears/core/namespaces.py,sha256=AjJljMOvHIf1RPrlEPf0Yt0bt2HJ-xvGn2Pf_61HztI,6746
7
+ threetears/core/pagination.py,sha256=hh2zzWsXalYkpKeu0kvLHLSnHuoXEveEXTKUUdwgvVI,8710
8
+ threetears/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ threetears/core/serialization.py,sha256=kXyr1n_Pth7H7HKYXvdWdphz2aHLYR86kSBKkeDh6vY,8174
10
+ threetears/core/backends/__init__.py,sha256=Cd2i4XCC9NoD8yNBKGjc44JuyzhXvVocg7MfIJxUMXI,466
11
+ threetears/core/backends/nats_proxy.py,sha256=zsE8qIlbnxMCVIxRDaqKNPpea2hUDMdsdaGuDnzhhhw,45219
12
+ threetears/core/backends/protocol.py,sha256=rjK8Eti8yOQirQ7f4O6dgRm3tm4t3RQN0DY3K2aLbWg,8544
13
+ threetears/core/backends/schema_sql.py,sha256=kzFZVZAFoEpr6DtkGGeGAF_3tHOV3fmhkxSjwsp2zuY,26528
14
+ threetears/core/backends/sql.py,sha256=SM_SVRdlZWpo2l3_uZBO7InkmjS5BlXFvx3n-Au5cv0,22466
15
+ threetears/core/cache/__init__.py,sha256=ogytyqLU788XI9V3U-bs7VPV8RizCHttvqUWOZ7sUf8,213
16
+ threetears/core/cache/base.py,sha256=CW4IzztA5PdD9ibJnk8Evm0EMgXcz8o5WhiY51Lf_Yc,4424
17
+ threetears/core/cache/duckdb.py,sha256=avwNz2Zfr0lXUHjcJPMsUK3GC1qXD4qhNCUyzlSA0ow,15640
18
+ threetears/core/cache/kv.py,sha256=XL9cp-tKTwiU-u_ffEH4Q9rP-a2Pezn7tx_euhwD6q0,15695
19
+ threetears/core/cache/sqlite.py,sha256=bxsTkT80xf5xJGUUVYLhWpAwBYcbs_kVx5X6ov1k7nM,22845
20
+ threetears/core/collections/__init__.py,sha256=GH8lOwvUkOUowbas9PsbhgX5bfltW9QpfoZkIvTIOaI,1513
21
+ threetears/core/collections/asyncpg_init.py,sha256=OXSH3ut1NZSKWbooGhiJjQIeppsDNy_C53bbCY41A0s,2964
22
+ threetears/core/collections/base.py,sha256=p_YSvGQ5CTTBsZLg0U4RTumqNfhrDpbhScYNBNAgoXM,50404
23
+ threetears/core/collections/durable_store.py,sha256=aVmj3taLR1A_U52uL8g1heXBpeAebqPU5DT57fmwCHU,4487
24
+ threetears/core/collections/flush.py,sha256=55e0am_dCE7BKII3o0r-IEkPp9y7iU-UnYOUaXXgUFQ,19343
25
+ threetears/core/collections/merge.py,sha256=dr6ghTEsFREgAxq205qSf7E3O-aQvA66blFXCNp4C3I,3284
26
+ threetears/core/collections/registry.py,sha256=KhrjhsU6cEcRW2ttwqyWJuvMMHFianD4Z39H5bN4_bM,15204
27
+ threetears/core/collections/schema_backed.py,sha256=lK4Zzq0FYCreVCR39wxOdwTkvb6F7-9DItSwiyQMzkk,67628
28
+ threetears/core/coordination/__init__.py,sha256=2npblE9Rz8Fhc2gVQgzL_Vf4FeLu6Eb8X45VpGefh6U,882
29
+ threetears/core/coordination/lease.py,sha256=ul7as2Rk4kb7vFbUG_OYQbJnCNc003WTGvXu6S-NWdA,21829
30
+ threetears/core/coordination/replay_guard.py,sha256=Vz6fAxzzs_2dHynF5FbznUs0j7guL-r3M9rK4qbjpSc,6148
31
+ threetears/core/data/__init__.py,sha256=i2lF_ISys1DT0WL62VhAcmZWg5kCJt2YqVWkZxcuAZ0,692
32
+ threetears/core/data/collection_factory.py,sha256=CrWi3Ug-hshgjx_fdEoJHOd8uXZf5XR0z01elVj3CyU,13181
33
+ threetears/core/data/schema.py,sha256=Th-AkNqL7dnqRD9mvaxkVSueE0Juy3qn3WIF1RpB5Rk,10595
34
+ threetears/core/data/sql_builder.py,sha256=hBsM7mRl401uokqp4kCwzAYKx4jq2UtYLu7bXwH6KAA,4451
35
+ threetears/core/data/store.py,sha256=6jCvQmmxpgKU7bMfUfeHJJCJTJ2EFz9yzYT24sxieDg,6658
36
+ threetears/core/data/migrations/README.md,sha256=K4g_493ZseWpPkC6JanYzXEi-3dYlI7D8hJ7CAfxRP4,5880
37
+ threetears/core/data/migrations/__init__.py,sha256=dthh1-xDVr1skRun8vlN10lzv-gp_w75bqQJGuzflYg,2528
38
+ threetears/core/data/migrations/drift.py,sha256=z5ICxZGTUbopGlLq4zsHIvLc7ivMMm8r6ijAzszg_5c,19205
39
+ threetears/core/data/migrations/enforcement.py,sha256=SqUPhX1YE8VxLLKEFqFCK90V3Kp9Hk6uay5t6ZD8pj8,30355
40
+ threetears/core/data/migrations/errors.py,sha256=fR5nJz7JmUJrrF1-iBhkAfk3nunUp_eAUOYIKA23-BU,2167
41
+ threetears/core/data/migrations/helpers.py,sha256=re8No56Ho4TNL_T1pqTeGwj8q3-20JugX9JXjMEsTYw,32290
42
+ threetears/core/data/migrations/preview.py,sha256=wpzs4WzDcOlo4utljvZKZFpj7Q_TVDalPgOp4LNMk8E,5710
43
+ threetears/core/data/migrations/registry.py,sha256=PFcb1J0QqhSN8gFZnZT1pN4m-IdPbZqBJaOSay38tnU,7000
44
+ threetears/core/data/migrations/runner.py,sha256=t9AmXLMfSnO--ju48DdHvO_DfGtTScck5QSPsf24eE0,28397
45
+ threetears/core/data/migrations/scope.py,sha256=rSjzT8mnlz5w0v6iK6mEhksiEu01OjOIspoWFedAUyk,940
46
+ threetears/core/data/migrations/template.py,sha256=wV4La9s07MKCM3pcKDmJBw4aZLFl317a3AJasz7KvG8,4155
47
+ threetears/core/entities/__init__.py,sha256=pg_ZfKCl985QE3I4cULHtdZYqHrISaNbVtsC9U-AgHM,79
48
+ threetears/core/entities/base.py,sha256=8f7Q72RllkUtR2RQvAVGomABBUy6GUo3q2mx8QRQQJ8,9913
49
+ threetears/core/security/__init__.py,sha256=N-9MURzzY2w7OiFd-V9Sg1jXVZgLm8GdPuooQsO_Dpo,3223
50
+ threetears/core/security/encryption.py,sha256=-JYPdwhz8qgRIu4RjcmZJIku07TMQz1iHJLfheiR6Dc,4841
51
+ threetears/core/security/identity_token.py,sha256=plzaySeuCsn1tJWSVg2DBlXcEUAlWSVVGxfojhyKGeQ,15976
52
+ threetears/core/security/jwks_provider.py,sha256=Y27IPXp3qWrzZcJZ2sITnB4GeNgc3UuP6CIkrLE5TGU,12403
53
+ threetears/core/security/pop.py,sha256=yrJ1zkonxD2gnP_8nIPFtmcXI-9LmT9aTL_m7IayrEk,7128
54
+ threetears/core/security/proxy_assertion.py,sha256=qyTvpV-FYf57sdJGY-rTowupKQEym3W2PRU6jsyS2H0,7664
55
+ threetears/core/security/proxy_signer.py,sha256=fA3RYtwnYXRXZMhGRc-vbHs1EiIfO6-_HybZxvADsjU,4988
56
+ threetears/core/security/sandbox.py,sha256=HzzUOh4Dc4KZTaBHedx1VIaC6w24LG7wMlOD0S2v-qU,17293
57
+ threetears/core/security/secret_refs.py,sha256=qTrdgZml-OF426x4MHPYTeDemF02HUcBAEZOV4XwpYo,9055
58
+ threetears/core/testing/__init__.py,sha256=Qh80JK-hplWOTYx6aXx10K3MKDPNz1IBfJ0gwntqb_0,1933
59
+ threetears/core/testing/containers.py,sha256=C9xgKyqKMvCl3Cfwo4Oo6PNoVmUx27GnVXiWPQ2PSI0,5132
60
+ threetears/core/testing/fixtures.py,sha256=urGJ_vKCuBoTvL8tvSJ4YJl7kiYKiLBnqYnFsv_PsMw,5417
61
+ threetears/core/testing/sqla_parity.py,sha256=it5hNUhRHJbTsihUstaQILeR8gN3ucA-_dNtp2oZo-M,13730
62
+ threetears/core/utils/__init__.py,sha256=5J7nVqjGILm4OTuDXXWM1eIO3eqeNxi2bsCQlPRgBQY,850
63
+ threetears/core/utils/atomic_write.py,sha256=ka3Zcp8iOVCIYHsmPsSkvsXPzKDVCU0vnVPVFmRPDhs,2887
64
+ threetears/core/utils/pg_pool_kwargs.py,sha256=NzbDA3eJzLhkeiuKdh2xGf7v3hbWDyNeKfnROdZS2ws,13503
65
+ threetears/knowledge/__init__.py,sha256=F8nzHbO0b9m2AQC6ZlXucLicGb3a5nDU8iG-z1J2CUs,2390
66
+ threetears/knowledge/chains.py,sha256=vmxuQxY3j4G5O53pP5WN0vniT7seJFlKktFMYKLBKzY,8331
67
+ threetears/knowledge/concept_merge.py,sha256=YXFx3RoR1TyNLEi8KX4JBtBLDmRjfRHvYDBAHn6t0tg,12189
68
+ threetears/knowledge/events.py,sha256=4dINoD6ShwaxJd0Gq5BGdoLbdkq5YZKAJdrow6_i_4E,9398
69
+ threetears/knowledge/merge.py,sha256=iCigU-AyzvZ7qkc6EL--8jvY0m6kLQnlbE1-JpSHWLU,13915
70
+ threetears/knowledge/scope.py,sha256=PrMzejWMKVOxxHu8Cpr3e7V_laFHlS5fMtSjPj-owpg,3470
71
+ 3tears-0.14.0.dist-info/METADATA,sha256=xZFiUGGW9IYLq8exv8et4HBVAfTFctNt7NxbmN7ukLI,8438
72
+ 3tears-0.14.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
73
+ 3tears-0.14.0.dist-info/licenses/LICENSE,sha256=7GWEoEOcFJenZLt4LDzqH2K7QLxo_2m8rzG7Vv8VGXo,1066
74
+ 3tears-0.14.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Pace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,170 @@
1
+ """threetears.core — three-tier caching framework.
2
+
3
+ Public API re-exports for convenient top-level imports.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ # Version derived from pyproject.toml so the metadata is the single
9
+ # source of truth -- a future release that bumps pyproject without
10
+ # updating ``__init__.py`` can't drift the runtime ``__version__``.
11
+ # The except guard handles the rare case where the package isn't
12
+ # installed via importlib.metadata (e.g. running directly from a
13
+ # checked-out source tree without ``uv sync``); the fallback keeps
14
+ # imports working but reports ``unknown`` rather than crashing.
15
+ from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
16
+ from importlib.metadata import version as _version
17
+ from typing import TYPE_CHECKING
18
+
19
+ try:
20
+ __version__ = _version("3tears")
21
+ except _PackageNotFoundError: # pragma: no cover - dev fallback
22
+ __version__ = "unknown"
23
+
24
+ # lazy public API (PEP 562). the package namespace no longer imports its
25
+ # implementation modules eagerly: importing this package (or any of its
26
+ # submodules) costs only this file, and each public attribute resolves
27
+ # its defining module on first access. the TYPE_CHECKING block carries
28
+ # the real imports so mypy and IDEs see the full statically-typed API;
29
+ # the _LAZY map is the runtime equivalent. the three-way agreement
30
+ # between __all__, _LAZY, and the TYPE_CHECKING block is pinned by the
31
+ # package's lazy-surface consistency test.
32
+ # decision record: docs/separate-concerns-decisions.md (hand-rolled
33
+ # PEP 562 over lazy_loader -- zero added runtime deps, no stub drift).
34
+ if TYPE_CHECKING:
35
+ from threetears.core.collections.base import BaseCollection
36
+ from threetears.core.collections.registry import CollectionRegistry
37
+ from threetears.core.config import CoreConfig, DefaultCoreConfig
38
+ from threetears.core.coordination import KVLease, LeaseHandle, LeaseLost, LeaseTimeout, LeaseUnavailable
39
+ from threetears.core.data.collection_factory import create_dynamic_collection
40
+ from threetears.core.data.migrations import MigrationRunner
41
+ from threetears.core.data.schema import ColumnDef, ForeignKeyDef, IndexDef, TableDef
42
+ from threetears.core.data.sql_builder import build_create_index_sql, build_create_table_sql
43
+ from threetears.core.data.store import DataStore
44
+ from threetears.core.entities.base import BaseEntity
45
+ from threetears.core.exceptions import ConcurrentModificationError, DataLayerUnavailableError
46
+ from threetears.core.namespaces import PLURAL_PREFIX_BY_NAMESPACE_TYPE, build_namespace_name, sanitize_segment
47
+ from threetears.core.pagination import CursorError, Keyset, Page, decode_cursor, encode_cursor
48
+ from threetears.core.security import PathSandbox, Sandbox, SandboxDecision, SandboxDenied
49
+ from threetears.core.serialization import (
50
+ FormatHandler,
51
+ UnknownFormatError,
52
+ deserialize_from_json,
53
+ handler_for,
54
+ register_handler,
55
+ serialize_to_json,
56
+ )
57
+ from threetears.core.utils.atomic_write import atomic_write
58
+
59
+ # public attribute -> (defining module, attribute name in that module)
60
+ _LAZY: dict[str, tuple[str, str]] = {
61
+ "BaseCollection": ("threetears.core.collections.base", "BaseCollection"),
62
+ "BaseEntity": ("threetears.core.entities.base", "BaseEntity"),
63
+ "CollectionRegistry": ("threetears.core.collections.registry", "CollectionRegistry"),
64
+ "ColumnDef": ("threetears.core.data.schema", "ColumnDef"),
65
+ "ConcurrentModificationError": ("threetears.core.exceptions", "ConcurrentModificationError"),
66
+ "CoreConfig": ("threetears.core.config", "CoreConfig"),
67
+ "CursorError": ("threetears.core.pagination", "CursorError"),
68
+ "DataLayerUnavailableError": ("threetears.core.exceptions", "DataLayerUnavailableError"),
69
+ "DataStore": ("threetears.core.data.store", "DataStore"),
70
+ "DefaultCoreConfig": ("threetears.core.config", "DefaultCoreConfig"),
71
+ "ForeignKeyDef": ("threetears.core.data.schema", "ForeignKeyDef"),
72
+ "FormatHandler": ("threetears.core.serialization", "FormatHandler"),
73
+ "IndexDef": ("threetears.core.data.schema", "IndexDef"),
74
+ "KVLease": ("threetears.core.coordination", "KVLease"),
75
+ "Keyset": ("threetears.core.pagination", "Keyset"),
76
+ "LeaseHandle": ("threetears.core.coordination", "LeaseHandle"),
77
+ "LeaseLost": ("threetears.core.coordination", "LeaseLost"),
78
+ "LeaseTimeout": ("threetears.core.coordination", "LeaseTimeout"),
79
+ "LeaseUnavailable": ("threetears.core.coordination", "LeaseUnavailable"),
80
+ "MigrationRunner": ("threetears.core.data.migrations", "MigrationRunner"),
81
+ "PLURAL_PREFIX_BY_NAMESPACE_TYPE": ("threetears.core.namespaces", "PLURAL_PREFIX_BY_NAMESPACE_TYPE"),
82
+ "Page": ("threetears.core.pagination", "Page"),
83
+ "PathSandbox": ("threetears.core.security", "PathSandbox"),
84
+ "Sandbox": ("threetears.core.security", "Sandbox"),
85
+ "SandboxDecision": ("threetears.core.security", "SandboxDecision"),
86
+ "SandboxDenied": ("threetears.core.security", "SandboxDenied"),
87
+ "TableDef": ("threetears.core.data.schema", "TableDef"),
88
+ "UnknownFormatError": ("threetears.core.serialization", "UnknownFormatError"),
89
+ "atomic_write": ("threetears.core.utils.atomic_write", "atomic_write"),
90
+ "build_create_index_sql": ("threetears.core.data.sql_builder", "build_create_index_sql"),
91
+ "build_create_table_sql": ("threetears.core.data.sql_builder", "build_create_table_sql"),
92
+ "build_namespace_name": ("threetears.core.namespaces", "build_namespace_name"),
93
+ "create_dynamic_collection": ("threetears.core.data.collection_factory", "create_dynamic_collection"),
94
+ "decode_cursor": ("threetears.core.pagination", "decode_cursor"),
95
+ "deserialize_from_json": ("threetears.core.serialization", "deserialize_from_json"),
96
+ "encode_cursor": ("threetears.core.pagination", "encode_cursor"),
97
+ "handler_for": ("threetears.core.serialization", "handler_for"),
98
+ "register_handler": ("threetears.core.serialization", "register_handler"),
99
+ "sanitize_segment": ("threetears.core.namespaces", "sanitize_segment"),
100
+ "serialize_to_json": ("threetears.core.serialization", "serialize_to_json"),
101
+ }
102
+
103
+ __all__ = [
104
+ "BaseCollection",
105
+ "BaseEntity",
106
+ "CollectionRegistry",
107
+ "ColumnDef",
108
+ "ConcurrentModificationError",
109
+ "CoreConfig",
110
+ "CursorError",
111
+ "DataLayerUnavailableError",
112
+ "DataStore",
113
+ "DefaultCoreConfig",
114
+ "ForeignKeyDef",
115
+ "FormatHandler",
116
+ "IndexDef",
117
+ "KVLease",
118
+ "LeaseHandle",
119
+ "LeaseLost",
120
+ "LeaseTimeout",
121
+ "LeaseUnavailable",
122
+ "MigrationRunner",
123
+ "PLURAL_PREFIX_BY_NAMESPACE_TYPE",
124
+ "PathSandbox",
125
+ "Sandbox",
126
+ "SandboxDecision",
127
+ "SandboxDenied",
128
+ "TableDef",
129
+ "UnknownFormatError",
130
+ "atomic_write",
131
+ "build_create_index_sql",
132
+ "build_create_table_sql",
133
+ "build_namespace_name",
134
+ "create_dynamic_collection",
135
+ "deserialize_from_json",
136
+ "handler_for",
137
+ "register_handler",
138
+ "sanitize_segment",
139
+ "serialize_to_json",
140
+ ]
141
+
142
+
143
+ def __getattr__(name: str) -> object:
144
+ """resolve a public attribute from its defining module on first access.
145
+
146
+ :param name: attribute name being resolved
147
+ :ptype name: str
148
+ :return: the resolved attribute (also cached in module globals so
149
+ ``__getattr__`` fires at most once per name)
150
+ :rtype: object
151
+ :raises AttributeError: when ``name`` is not part of the public API
152
+ """
153
+ entry = _LAZY.get(name)
154
+ if entry is None:
155
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
156
+ from importlib import import_module
157
+
158
+ module_name, attr = entry
159
+ value: object = getattr(import_module(module_name), attr)
160
+ globals()[name] = value
161
+ return value
162
+
163
+
164
+ def __dir__() -> list[str]:
165
+ """include lazy attributes in ``dir()`` output.
166
+
167
+ :return: sorted union of materialized globals and lazy names
168
+ :rtype: list[str]
169
+ """
170
+ return sorted(set(globals()) | set(_LAZY))
@@ -0,0 +1,102 @@
1
+ """Background event loop for sync-to-async bridging.
2
+
3
+ Provides a singleton daemon thread running its own event loop. Sync code
4
+ (like __getitem__) submits async coroutines via run_coroutine_threadsafe
5
+ and blocks on the result.
6
+
7
+ ``fire_and_forget`` is loop-aware: when called from a thread that already
8
+ has a running event loop (e.g., an ASGI handler), it schedules the task on
9
+ that loop via ``create_task`` so that async resources (e.g., asyncpg
10
+ connection pools) stay on the correct loop. When called from pure sync code
11
+ with no running loop, it falls back to the background loop.
12
+
13
+ Pattern borrowed from fsspec (pandas/dask/xarray ecosystem).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import threading
20
+ from typing import Any, Coroutine, TypeVar
21
+
22
+ __all__ = [
23
+ "T",
24
+ "drain",
25
+ "fire_and_forget",
26
+ "shutdown",
27
+ "sync_await",
28
+ ]
29
+
30
+ T = TypeVar("T")
31
+
32
+ _lock = threading.Lock()
33
+ _loop: asyncio.AbstractEventLoop | None = None
34
+ _thread: threading.Thread | None = None
35
+
36
+
37
+ def _ensure_loop() -> asyncio.AbstractEventLoop:
38
+ """Lazily start the background event loop on first use."""
39
+ global _loop, _thread
40
+ if _loop is not None and _loop.is_running():
41
+ return _loop
42
+ with _lock:
43
+ if _loop is not None and _loop.is_running():
44
+ return _loop
45
+ _loop = asyncio.new_event_loop()
46
+ _thread = threading.Thread(
47
+ target=_loop.run_forever,
48
+ daemon=True,
49
+ name="threetears-async-bridge",
50
+ )
51
+ _thread.start()
52
+ return _loop
53
+
54
+
55
+ def sync_await(coro: Coroutine[Any, Any, T]) -> T:
56
+ """Run an async coroutine from sync code, blocking until complete."""
57
+ loop = _ensure_loop()
58
+ future = asyncio.run_coroutine_threadsafe(coro, loop)
59
+ return future.result()
60
+
61
+
62
+ def fire_and_forget(coro: Coroutine[Any, Any, Any]) -> None:
63
+ """Submit an async coroutine without blocking.
64
+
65
+ When called from a thread with a running event loop, schedules the task
66
+ on that loop (``create_task``). When called from pure sync code, uses
67
+ the background loop (``run_coroutine_threadsafe``).
68
+ """
69
+ try:
70
+ loop = asyncio.get_running_loop()
71
+ loop.create_task(coro)
72
+ except RuntimeError:
73
+ bg_loop = _ensure_loop()
74
+ asyncio.run_coroutine_threadsafe(coro, bg_loop)
75
+
76
+
77
+ def drain() -> None:
78
+ """Wait for all pending tasks on the background loop to complete."""
79
+ if _loop is None or not _loop.is_running():
80
+ return
81
+
82
+ async def _drain() -> None:
83
+ # Get all tasks on this loop and wait for them
84
+ tasks = [t for t in asyncio.all_tasks(_loop) if not t.done() and t is not asyncio.current_task()]
85
+ if tasks:
86
+ await asyncio.gather(*tasks, return_exceptions=True)
87
+
88
+ future = asyncio.run_coroutine_threadsafe(_drain(), _loop)
89
+ future.result(timeout=10)
90
+
91
+
92
+ def shutdown() -> None:
93
+ """Stop the background loop and join the thread. For clean teardown."""
94
+ global _loop, _thread
95
+ if _loop is not None and _loop.is_running():
96
+ _loop.call_soon_threadsafe(_loop.stop)
97
+ if _thread is not None:
98
+ _thread.join(timeout=5)
99
+ _thread = None
100
+ if _loop is not None:
101
+ _loop.close()
102
+ _loop = None
@@ -0,0 +1,16 @@
1
+ """L3 durable-tier backends + protocols."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from threetears.core.backends.nats_proxy import NatsProxyL3Backend
6
+ from threetears.core.backends.protocol import DurableStore, L3Backend, parse_rowcount
7
+ from threetears.core.backends.sql import SqlL3Backend, bound_request_connection
8
+
9
+ __all__ = [
10
+ "DurableStore",
11
+ "L3Backend",
12
+ "NatsProxyL3Backend",
13
+ "SqlL3Backend",
14
+ "bound_request_connection",
15
+ "parse_rowcount",
16
+ ]