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.
- 3tears-0.14.0.dist-info/METADATA +260 -0
- 3tears-0.14.0.dist-info/RECORD +74 -0
- 3tears-0.14.0.dist-info/WHEEL +4 -0
- 3tears-0.14.0.dist-info/licenses/LICENSE +21 -0
- threetears/core/__init__.py +170 -0
- threetears/core/_bridge.py +102 -0
- threetears/core/backends/__init__.py +16 -0
- threetears/core/backends/nats_proxy.py +1125 -0
- threetears/core/backends/protocol.py +186 -0
- threetears/core/backends/schema_sql.py +705 -0
- threetears/core/backends/sql.py +446 -0
- threetears/core/cache/__init__.py +6 -0
- threetears/core/cache/base.py +131 -0
- threetears/core/cache/duckdb.py +419 -0
- threetears/core/cache/kv.py +436 -0
- threetears/core/cache/sqlite.py +578 -0
- threetears/core/collections/__init__.py +58 -0
- threetears/core/collections/asyncpg_init.py +90 -0
- threetears/core/collections/base.py +1169 -0
- threetears/core/collections/durable_store.py +92 -0
- threetears/core/collections/flush.py +460 -0
- threetears/core/collections/merge.py +79 -0
- threetears/core/collections/registry.py +364 -0
- threetears/core/collections/schema_backed.py +1575 -0
- threetears/core/config.py +38 -0
- threetears/core/coordination/__init__.py +29 -0
- threetears/core/coordination/lease.py +563 -0
- threetears/core/coordination/replay_guard.py +125 -0
- threetears/core/data/__init__.py +21 -0
- threetears/core/data/collection_factory.py +369 -0
- threetears/core/data/migrations/README.md +191 -0
- threetears/core/data/migrations/__init__.py +83 -0
- threetears/core/data/migrations/drift.py +513 -0
- threetears/core/data/migrations/enforcement.py +850 -0
- threetears/core/data/migrations/errors.py +68 -0
- threetears/core/data/migrations/helpers.py +888 -0
- threetears/core/data/migrations/preview.py +154 -0
- threetears/core/data/migrations/registry.py +200 -0
- threetears/core/data/migrations/runner.py +686 -0
- threetears/core/data/migrations/scope.py +33 -0
- threetears/core/data/migrations/template.py +135 -0
- threetears/core/data/schema.py +327 -0
- threetears/core/data/sql_builder.py +127 -0
- threetears/core/data/store.py +167 -0
- threetears/core/entities/__init__.py +3 -0
- threetears/core/entities/base.py +243 -0
- threetears/core/exceptions.py +29 -0
- threetears/core/models.py +111 -0
- threetears/core/namespaces.py +168 -0
- threetears/core/pagination.py +188 -0
- threetears/core/py.typed +0 -0
- threetears/core/security/__init__.py +94 -0
- threetears/core/security/encryption.py +97 -0
- threetears/core/security/identity_token.py +328 -0
- threetears/core/security/jwks_provider.py +239 -0
- threetears/core/security/pop.py +174 -0
- threetears/core/security/proxy_assertion.py +196 -0
- threetears/core/security/proxy_signer.py +131 -0
- threetears/core/security/sandbox.py +436 -0
- threetears/core/security/secret_refs.py +226 -0
- threetears/core/serialization.py +234 -0
- threetears/core/testing/__init__.py +57 -0
- threetears/core/testing/containers.py +157 -0
- threetears/core/testing/fixtures.py +149 -0
- threetears/core/testing/sqla_parity.py +339 -0
- threetears/core/utils/__init__.py +29 -0
- threetears/core/utils/atomic_write.py +85 -0
- threetears/core/utils/pg_pool_kwargs.py +366 -0
- threetears/knowledge/__init__.py +78 -0
- threetears/knowledge/chains.py +215 -0
- threetears/knowledge/concept_merge.py +277 -0
- threetears/knowledge/events.py +224 -0
- threetears/knowledge/merge.py +331 -0
- 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,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
|
+
]
|