cortex-agent-sdk 0.0.1__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.
- cortex_agent_sdk/__init__.py +7 -0
- cortex_agent_sdk/agent.py +321 -0
- cortex_agent_sdk/engine.py +150 -0
- cortex_agent_sdk/errores/__init__.py +2 -0
- cortex_agent_sdk/errores/catalogo.py +35 -0
- cortex_agent_sdk/errores/excepcion.py +19 -0
- cortex_agent_sdk/gateway.py +23 -0
- cortex_agent_sdk/google/__init__.py +1 -0
- cortex_agent_sdk/history/__init__.py +2 -0
- cortex_agent_sdk/history/models.py +145 -0
- cortex_agent_sdk/history/pipeline.py +53 -0
- cortex_agent_sdk/history/transform.py +17 -0
- cortex_agent_sdk/hooks.py +102 -0
- cortex_agent_sdk/immutable.py +54 -0
- cortex_agent_sdk/lifecycle.py +67 -0
- cortex_agent_sdk/openai/__init__.py +2 -0
- cortex_agent_sdk/openai/engine.py +304 -0
- cortex_agent_sdk/openai/options.py +19 -0
- cortex_agent_sdk/postgres/__init__.py +1 -0
- cortex_agent_sdk/postgres/store.py +468 -0
- cortex_agent_sdk/py.typed +1 -0
- cortex_agent_sdk/redis/__init__.py +1 -0
- cortex_agent_sdk/redis/scripts.py +65 -0
- cortex_agent_sdk/redis/store.py +293 -0
- cortex_agent_sdk/results.py +29 -0
- cortex_agent_sdk/runtime.py +39 -0
- cortex_agent_sdk/sessions/__init__.py +4 -0
- cortex_agent_sdk/sessions/codec.py +27 -0
- cortex_agent_sdk/sessions/lease.py +85 -0
- cortex_agent_sdk/sessions/memory.py +186 -0
- cortex_agent_sdk/sessions/models.py +70 -0
- cortex_agent_sdk/sessions/store.py +38 -0
- cortex_agent_sdk/tools/__init__.py +1 -0
- cortex_agent_sdk/tools/contracts.py +152 -0
- cortex_agent_sdk/tools/decorators.py +15 -0
- cortex_agent_sdk/tools/execution.py +181 -0
- cortex_agent_sdk/tools/models.py +57 -0
- cortex_agent_sdk-0.0.1.dist-info/METADATA +192 -0
- cortex_agent_sdk-0.0.1.dist-info/RECORD +41 -0
- cortex_agent_sdk-0.0.1.dist-info/WHEEL +4 -0
- cortex_agent_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from contextlib import asynccontextmanager, suppress
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Self, cast
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
import asyncpg
|
|
11
|
+
from asyncpg import InterfaceError, Pool, PostgresError, Record
|
|
12
|
+
|
|
13
|
+
from cortex_agent_sdk.errores import AppError, CodigoError
|
|
14
|
+
from cortex_agent_sdk.sessions.codec import SessionCodec
|
|
15
|
+
from cortex_agent_sdk.sessions.models import SessionRecord
|
|
16
|
+
from cortex_agent_sdk.sessions.store import SessionLease
|
|
17
|
+
|
|
18
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,62}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _PostgresLease:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
store: "PostgresSessionStore",
|
|
25
|
+
session_id: str,
|
|
26
|
+
owner: str,
|
|
27
|
+
fencing_token: int,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._store = store
|
|
30
|
+
self._session_id = session_id
|
|
31
|
+
self._owner = owner
|
|
32
|
+
self._fencing_token = fencing_token
|
|
33
|
+
self._active = True
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def session_id(self) -> str:
|
|
37
|
+
return self._session_id
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def fencing_token(self) -> int:
|
|
41
|
+
return self._fencing_token
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def renewal_interval_seconds(self) -> float:
|
|
45
|
+
return self._store.renewal_interval_seconds
|
|
46
|
+
|
|
47
|
+
async def verify_ownership(self, fencing_token: int) -> None:
|
|
48
|
+
self._ensure_active(fencing_token)
|
|
49
|
+
await self._store._verify(self._session_id, self._owner, self._fencing_token)
|
|
50
|
+
|
|
51
|
+
async def renew(self, fencing_token: int) -> None:
|
|
52
|
+
self._ensure_active(fencing_token)
|
|
53
|
+
await self._store._renew(self._session_id, self._owner, self._fencing_token)
|
|
54
|
+
|
|
55
|
+
async def load(self, fencing_token: int) -> SessionRecord | None:
|
|
56
|
+
self._ensure_active(fencing_token)
|
|
57
|
+
return await self._store._load(self._session_id, self._owner, self._fencing_token)
|
|
58
|
+
|
|
59
|
+
async def save(self, record: SessionRecord, fencing_token: int) -> SessionRecord:
|
|
60
|
+
self._ensure_active(fencing_token)
|
|
61
|
+
if record.session_id != self._session_id:
|
|
62
|
+
raise AppError(CodigoError.SESION_INVALIDA, "session_id no coincide con el lease")
|
|
63
|
+
return await self._store._save(record, self._owner, self._fencing_token)
|
|
64
|
+
|
|
65
|
+
def close(self) -> None:
|
|
66
|
+
self._active = False
|
|
67
|
+
|
|
68
|
+
def _ensure_active(self, fencing_token: int) -> None:
|
|
69
|
+
if not self._active or fencing_token != self._fencing_token:
|
|
70
|
+
raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PostgresSessionStore:
|
|
74
|
+
"""Store persistente con lease renovable y CAS en Postgres."""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
dsn: str | None = None,
|
|
79
|
+
*,
|
|
80
|
+
pool: Pool | None = None,
|
|
81
|
+
table_name: str = "cortex_agent_sessions",
|
|
82
|
+
ttl_seconds: float = 86_400,
|
|
83
|
+
lease_seconds: float = 30,
|
|
84
|
+
retry_interval_seconds: float = 0.05,
|
|
85
|
+
io_timeout_seconds: float = 10,
|
|
86
|
+
) -> None:
|
|
87
|
+
if (dsn is None) == (pool is None):
|
|
88
|
+
raise AppError(CodigoError.CONFIG_INVALIDA, "indica dsn o pool de Postgres")
|
|
89
|
+
if _IDENTIFIER.fullmatch(table_name) is None:
|
|
90
|
+
raise AppError(CodigoError.CONFIG_INVALIDA, "nombre de tabla inválido")
|
|
91
|
+
durations = (ttl_seconds, lease_seconds, retry_interval_seconds, io_timeout_seconds)
|
|
92
|
+
if min(durations) <= 0:
|
|
93
|
+
raise AppError(CodigoError.CONFIG_INVALIDA, "configuración de Postgres inválida")
|
|
94
|
+
|
|
95
|
+
self._dsn = dsn
|
|
96
|
+
self._pool = pool
|
|
97
|
+
self._owns_pool = pool is None
|
|
98
|
+
self._table = f'"{table_name}"'
|
|
99
|
+
self._ttl_seconds = ttl_seconds
|
|
100
|
+
self._lease_seconds = lease_seconds
|
|
101
|
+
self._retry_interval_seconds = retry_interval_seconds
|
|
102
|
+
self._io_timeout_seconds = io_timeout_seconds
|
|
103
|
+
self._pool_lock = asyncio.Lock()
|
|
104
|
+
self._codec = SessionCodec()
|
|
105
|
+
self._closed = False
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def renewal_interval_seconds(self) -> float:
|
|
109
|
+
return self._lease_seconds / 3
|
|
110
|
+
|
|
111
|
+
async def __aenter__(self) -> Self:
|
|
112
|
+
self._ensure_open()
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
async def __aexit__(self, *_: object) -> None:
|
|
116
|
+
await self.aclose()
|
|
117
|
+
|
|
118
|
+
async def setup(self) -> None:
|
|
119
|
+
query = f"""
|
|
120
|
+
CREATE TABLE IF NOT EXISTS {self._table} (
|
|
121
|
+
session_id TEXT PRIMARY KEY,
|
|
122
|
+
payload BYTEA,
|
|
123
|
+
version BIGINT NOT NULL DEFAULT 0,
|
|
124
|
+
updated_at TIMESTAMPTZ,
|
|
125
|
+
expires_at TIMESTAMPTZ,
|
|
126
|
+
lease_owner TEXT,
|
|
127
|
+
fencing_token BIGINT NOT NULL DEFAULT 0,
|
|
128
|
+
lease_until TIMESTAMPTZ
|
|
129
|
+
)
|
|
130
|
+
"""
|
|
131
|
+
await self._execute(query)
|
|
132
|
+
|
|
133
|
+
async def cleanup_expired(self, limit: int = 1_000) -> int:
|
|
134
|
+
"""Borra payloads vencidos sin eliminar su fencing counter."""
|
|
135
|
+
if limit < 1:
|
|
136
|
+
raise AppError(CodigoError.CONFIG_INVALIDA, "limit debe ser positivo")
|
|
137
|
+
query = f"""
|
|
138
|
+
WITH expired AS (
|
|
139
|
+
SELECT session_id
|
|
140
|
+
FROM {self._table}
|
|
141
|
+
WHERE expires_at <= NOW()
|
|
142
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
143
|
+
ORDER BY expires_at
|
|
144
|
+
LIMIT $1
|
|
145
|
+
FOR UPDATE SKIP LOCKED
|
|
146
|
+
)
|
|
147
|
+
UPDATE {self._table} AS sessions
|
|
148
|
+
SET payload = NULL, version = 0, updated_at = NULL, expires_at = NULL
|
|
149
|
+
FROM expired
|
|
150
|
+
WHERE sessions.session_id = expired.session_id
|
|
151
|
+
"""
|
|
152
|
+
status = await self._execute(query, limit)
|
|
153
|
+
return int(status.rsplit(" ", 1)[-1])
|
|
154
|
+
|
|
155
|
+
@asynccontextmanager
|
|
156
|
+
async def acquire(
|
|
157
|
+
self,
|
|
158
|
+
session_id: str,
|
|
159
|
+
timeout_seconds: float,
|
|
160
|
+
) -> AsyncIterator[SessionLease]:
|
|
161
|
+
self._ensure_open()
|
|
162
|
+
if not session_id or timeout_seconds <= 0:
|
|
163
|
+
raise AppError(CodigoError.CONFIG_INVALIDA, "adquisición de sesión inválida")
|
|
164
|
+
|
|
165
|
+
owner = uuid4().hex
|
|
166
|
+
fencing_token = await self._acquire(session_id, owner, timeout_seconds)
|
|
167
|
+
lease = _PostgresLease(self, session_id, owner, fencing_token)
|
|
168
|
+
try:
|
|
169
|
+
yield lease
|
|
170
|
+
finally:
|
|
171
|
+
lease.close()
|
|
172
|
+
with suppress(AppError):
|
|
173
|
+
await self._release(session_id, owner, fencing_token)
|
|
174
|
+
|
|
175
|
+
async def reset(self, session_id: str, timeout_seconds: float = 5.0) -> None:
|
|
176
|
+
async with self.acquire(session_id, timeout_seconds) as lease:
|
|
177
|
+
if not isinstance(lease, _PostgresLease):
|
|
178
|
+
raise AppError(CodigoError.SESION_INVALIDA, "lease de Postgres inválido")
|
|
179
|
+
query = f"""
|
|
180
|
+
UPDATE {self._table}
|
|
181
|
+
SET payload = NULL, version = 0, updated_at = NULL, expires_at = NULL
|
|
182
|
+
WHERE session_id = $1
|
|
183
|
+
AND lease_owner = $2
|
|
184
|
+
AND fencing_token = $3
|
|
185
|
+
AND lease_until > NOW()
|
|
186
|
+
RETURNING fencing_token
|
|
187
|
+
"""
|
|
188
|
+
result = await self._fetchval(
|
|
189
|
+
query,
|
|
190
|
+
session_id,
|
|
191
|
+
lease._owner,
|
|
192
|
+
lease.fencing_token,
|
|
193
|
+
)
|
|
194
|
+
if result is None:
|
|
195
|
+
raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
|
|
196
|
+
|
|
197
|
+
async def aclose(self) -> None:
|
|
198
|
+
if self._closed:
|
|
199
|
+
return
|
|
200
|
+
if self._owns_pool and self._pool is not None:
|
|
201
|
+
try:
|
|
202
|
+
async with asyncio.timeout(self._io_timeout_seconds):
|
|
203
|
+
await self._pool.close()
|
|
204
|
+
except TimeoutError as error:
|
|
205
|
+
raise AppError(
|
|
206
|
+
CodigoError.SESION_STORE_FALLO,
|
|
207
|
+
"Postgres no cerró su pool dentro del timeout",
|
|
208
|
+
) from error
|
|
209
|
+
except (InterfaceError, PostgresError, OSError) as error:
|
|
210
|
+
raise _store_error(error) from error
|
|
211
|
+
self._closed = True
|
|
212
|
+
|
|
213
|
+
async def _acquire(self, session_id: str, owner: str, timeout_seconds: float) -> int:
|
|
214
|
+
query = f"""
|
|
215
|
+
INSERT INTO {self._table} AS sessions (
|
|
216
|
+
session_id, lease_owner, fencing_token, lease_until
|
|
217
|
+
)
|
|
218
|
+
VALUES ($1, $2, 1, NOW() + $3 * INTERVAL '1 second')
|
|
219
|
+
ON CONFLICT (session_id) DO UPDATE
|
|
220
|
+
SET lease_owner = EXCLUDED.lease_owner,
|
|
221
|
+
fencing_token = sessions.fencing_token + 1,
|
|
222
|
+
lease_until = EXCLUDED.lease_until
|
|
223
|
+
WHERE sessions.lease_until IS NULL OR sessions.lease_until <= NOW()
|
|
224
|
+
RETURNING fencing_token
|
|
225
|
+
"""
|
|
226
|
+
deadline = time.monotonic() + timeout_seconds
|
|
227
|
+
while True:
|
|
228
|
+
remaining = deadline - time.monotonic()
|
|
229
|
+
if remaining <= 0:
|
|
230
|
+
raise AppError(CodigoError.SESION_OCUPADA, f"sesión ocupada: {session_id}")
|
|
231
|
+
result = await self._fetchval(
|
|
232
|
+
query,
|
|
233
|
+
session_id,
|
|
234
|
+
owner,
|
|
235
|
+
self._lease_seconds,
|
|
236
|
+
timeout_seconds=remaining,
|
|
237
|
+
)
|
|
238
|
+
if result is not None:
|
|
239
|
+
return cast(int, result)
|
|
240
|
+
remaining = deadline - time.monotonic()
|
|
241
|
+
if remaining <= 0:
|
|
242
|
+
raise AppError(CodigoError.SESION_OCUPADA, f"sesión ocupada: {session_id}")
|
|
243
|
+
await asyncio.sleep(min(self._retry_interval_seconds, remaining))
|
|
244
|
+
|
|
245
|
+
async def _verify(self, session_id: str, owner: str, fencing_token: int) -> None:
|
|
246
|
+
query = f"""
|
|
247
|
+
SELECT EXISTS(
|
|
248
|
+
SELECT 1 FROM {self._table}
|
|
249
|
+
WHERE session_id = $1
|
|
250
|
+
AND lease_owner = $2
|
|
251
|
+
AND fencing_token = $3
|
|
252
|
+
AND lease_until > NOW()
|
|
253
|
+
)
|
|
254
|
+
"""
|
|
255
|
+
if not await self._fetchval(query, session_id, owner, fencing_token):
|
|
256
|
+
raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
|
|
257
|
+
|
|
258
|
+
async def _renew(self, session_id: str, owner: str, fencing_token: int) -> None:
|
|
259
|
+
query = f"""
|
|
260
|
+
UPDATE {self._table}
|
|
261
|
+
SET lease_until = NOW() + $4 * INTERVAL '1 second'
|
|
262
|
+
WHERE session_id = $1
|
|
263
|
+
AND lease_owner = $2
|
|
264
|
+
AND fencing_token = $3
|
|
265
|
+
AND lease_until > NOW()
|
|
266
|
+
RETURNING fencing_token
|
|
267
|
+
"""
|
|
268
|
+
result = await self._fetchval(
|
|
269
|
+
query,
|
|
270
|
+
session_id,
|
|
271
|
+
owner,
|
|
272
|
+
fencing_token,
|
|
273
|
+
self._lease_seconds,
|
|
274
|
+
)
|
|
275
|
+
if result is None:
|
|
276
|
+
raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
|
|
277
|
+
|
|
278
|
+
async def _load(
|
|
279
|
+
self,
|
|
280
|
+
session_id: str,
|
|
281
|
+
owner: str,
|
|
282
|
+
fencing_token: int,
|
|
283
|
+
) -> SessionRecord | None:
|
|
284
|
+
query = f"""
|
|
285
|
+
UPDATE {self._table}
|
|
286
|
+
SET payload = CASE
|
|
287
|
+
WHEN expires_at <= NOW() THEN NULL
|
|
288
|
+
ELSE payload
|
|
289
|
+
END,
|
|
290
|
+
version = CASE
|
|
291
|
+
WHEN expires_at <= NOW() THEN 0
|
|
292
|
+
ELSE version
|
|
293
|
+
END,
|
|
294
|
+
updated_at = CASE
|
|
295
|
+
WHEN expires_at <= NOW() THEN NULL
|
|
296
|
+
ELSE updated_at
|
|
297
|
+
END,
|
|
298
|
+
expires_at = CASE
|
|
299
|
+
WHEN payload IS NULL OR expires_at <= NOW() THEN NULL
|
|
300
|
+
ELSE NOW() + $4 * INTERVAL '1 second'
|
|
301
|
+
END
|
|
302
|
+
WHERE session_id = $1
|
|
303
|
+
AND lease_owner = $2
|
|
304
|
+
AND fencing_token = $3
|
|
305
|
+
AND lease_until > NOW()
|
|
306
|
+
RETURNING payload
|
|
307
|
+
"""
|
|
308
|
+
row = await self._fetchrow(
|
|
309
|
+
query,
|
|
310
|
+
session_id,
|
|
311
|
+
owner,
|
|
312
|
+
fencing_token,
|
|
313
|
+
self._ttl_seconds,
|
|
314
|
+
)
|
|
315
|
+
if row is None:
|
|
316
|
+
raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
|
|
317
|
+
payload = row["payload"]
|
|
318
|
+
if payload is None:
|
|
319
|
+
return None
|
|
320
|
+
return self._codec.decode(bytes(payload))
|
|
321
|
+
|
|
322
|
+
async def _save(
|
|
323
|
+
self,
|
|
324
|
+
record: SessionRecord,
|
|
325
|
+
owner: str,
|
|
326
|
+
fencing_token: int,
|
|
327
|
+
) -> SessionRecord:
|
|
328
|
+
saved = record.model_copy(
|
|
329
|
+
update={
|
|
330
|
+
"version": record.version + 1,
|
|
331
|
+
"updated_at": datetime.now(UTC),
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
query = f"""
|
|
335
|
+
UPDATE {self._table}
|
|
336
|
+
SET payload = $5,
|
|
337
|
+
version = $6,
|
|
338
|
+
updated_at = NOW(),
|
|
339
|
+
expires_at = NOW() + $7 * INTERVAL '1 second'
|
|
340
|
+
WHERE session_id = $1
|
|
341
|
+
AND lease_owner = $2
|
|
342
|
+
AND fencing_token = $3
|
|
343
|
+
AND lease_until > NOW()
|
|
344
|
+
AND version = $4
|
|
345
|
+
RETURNING version
|
|
346
|
+
"""
|
|
347
|
+
result = await self._fetchval(
|
|
348
|
+
query,
|
|
349
|
+
record.session_id,
|
|
350
|
+
owner,
|
|
351
|
+
fencing_token,
|
|
352
|
+
record.version,
|
|
353
|
+
self._codec.encode(saved),
|
|
354
|
+
saved.version,
|
|
355
|
+
self._ttl_seconds,
|
|
356
|
+
)
|
|
357
|
+
if result is not None:
|
|
358
|
+
return saved
|
|
359
|
+
await self._raise_save_failure(record, owner, fencing_token)
|
|
360
|
+
raise AssertionError("save failure no traducido")
|
|
361
|
+
|
|
362
|
+
async def _raise_save_failure(
|
|
363
|
+
self,
|
|
364
|
+
record: SessionRecord,
|
|
365
|
+
owner: str,
|
|
366
|
+
fencing_token: int,
|
|
367
|
+
) -> None:
|
|
368
|
+
query = f"""
|
|
369
|
+
SELECT version, lease_owner, fencing_token, lease_until > NOW() AS lease_active
|
|
370
|
+
FROM {self._table}
|
|
371
|
+
WHERE session_id = $1
|
|
372
|
+
"""
|
|
373
|
+
row = await self._fetchrow(query, record.session_id)
|
|
374
|
+
owns_lease = (
|
|
375
|
+
row is not None
|
|
376
|
+
and row["lease_owner"] == owner
|
|
377
|
+
and row["fencing_token"] == fencing_token
|
|
378
|
+
and row["lease_active"]
|
|
379
|
+
)
|
|
380
|
+
if owns_lease:
|
|
381
|
+
raise AppError(CodigoError.SESION_CONFLICTO, "la versión de sesión cambió")
|
|
382
|
+
raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
|
|
383
|
+
|
|
384
|
+
async def _release(self, session_id: str, owner: str, fencing_token: int) -> None:
|
|
385
|
+
query = f"""
|
|
386
|
+
UPDATE {self._table}
|
|
387
|
+
SET lease_owner = NULL, lease_until = NULL
|
|
388
|
+
WHERE session_id = $1 AND lease_owner = $2 AND fencing_token = $3
|
|
389
|
+
"""
|
|
390
|
+
await self._execute(query, session_id, owner, fencing_token)
|
|
391
|
+
|
|
392
|
+
async def _get_pool(self) -> Pool:
|
|
393
|
+
self._ensure_open()
|
|
394
|
+
if self._pool is not None:
|
|
395
|
+
return self._pool
|
|
396
|
+
async with self._pool_lock:
|
|
397
|
+
if self._pool is None:
|
|
398
|
+
try:
|
|
399
|
+
self._pool = await asyncpg.create_pool(dsn=cast(str, self._dsn))
|
|
400
|
+
except (InterfaceError, PostgresError, OSError) as error:
|
|
401
|
+
raise _store_error(error) from error
|
|
402
|
+
return self._pool
|
|
403
|
+
|
|
404
|
+
async def _fetchval(
|
|
405
|
+
self,
|
|
406
|
+
query: str,
|
|
407
|
+
*arguments: object,
|
|
408
|
+
timeout_seconds: float | None = None,
|
|
409
|
+
) -> object:
|
|
410
|
+
timeout = timeout_seconds or self._io_timeout_seconds
|
|
411
|
+
try:
|
|
412
|
+
async with asyncio.timeout(min(timeout, self._io_timeout_seconds)):
|
|
413
|
+
return await (await self._get_pool()).fetchval(query, *arguments)
|
|
414
|
+
except TimeoutError as error:
|
|
415
|
+
raise AppError(
|
|
416
|
+
CodigoError.SESION_STORE_FALLO,
|
|
417
|
+
"Postgres agotó el timeout de I/O",
|
|
418
|
+
) from error
|
|
419
|
+
except (InterfaceError, PostgresError, OSError) as error:
|
|
420
|
+
raise _store_error(error) from error
|
|
421
|
+
|
|
422
|
+
async def _fetchrow(
|
|
423
|
+
self,
|
|
424
|
+
query: str,
|
|
425
|
+
*arguments: object,
|
|
426
|
+
timeout_seconds: float | None = None,
|
|
427
|
+
) -> Record | None:
|
|
428
|
+
timeout = timeout_seconds or self._io_timeout_seconds
|
|
429
|
+
try:
|
|
430
|
+
async with asyncio.timeout(min(timeout, self._io_timeout_seconds)):
|
|
431
|
+
return await (await self._get_pool()).fetchrow(query, *arguments)
|
|
432
|
+
except TimeoutError as error:
|
|
433
|
+
raise AppError(
|
|
434
|
+
CodigoError.SESION_STORE_FALLO,
|
|
435
|
+
"Postgres agotó el timeout de I/O",
|
|
436
|
+
) from error
|
|
437
|
+
except (InterfaceError, PostgresError, OSError) as error:
|
|
438
|
+
raise _store_error(error) from error
|
|
439
|
+
|
|
440
|
+
async def _execute(
|
|
441
|
+
self,
|
|
442
|
+
query: str,
|
|
443
|
+
*arguments: object,
|
|
444
|
+
timeout_seconds: float | None = None,
|
|
445
|
+
) -> str:
|
|
446
|
+
timeout = timeout_seconds or self._io_timeout_seconds
|
|
447
|
+
try:
|
|
448
|
+
async with asyncio.timeout(min(timeout, self._io_timeout_seconds)):
|
|
449
|
+
return await (await self._get_pool()).execute(query, *arguments)
|
|
450
|
+
except TimeoutError as error:
|
|
451
|
+
raise AppError(
|
|
452
|
+
CodigoError.SESION_STORE_FALLO,
|
|
453
|
+
"Postgres agotó el timeout de I/O",
|
|
454
|
+
) from error
|
|
455
|
+
except (InterfaceError, PostgresError, OSError) as error:
|
|
456
|
+
raise _store_error(error) from error
|
|
457
|
+
|
|
458
|
+
def _ensure_open(self) -> None:
|
|
459
|
+
if self._closed:
|
|
460
|
+
raise AppError(CodigoError.RECURSO_CERRADO, "store Postgres cerrado")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _store_error(error: BaseException) -> AppError:
|
|
464
|
+
return AppError(
|
|
465
|
+
CodigoError.SESION_STORE_FALLO,
|
|
466
|
+
f"Postgres lanzó {type(error).__name__}",
|
|
467
|
+
)
|
|
468
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from cortex_agent_sdk.redis.store import RedisSessionStore
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
ACQUIRE = """
|
|
2
|
+
if redis.call('exists', KEYS[1]) == 0 then
|
|
3
|
+
local token = redis.call('incr', KEYS[2])
|
|
4
|
+
redis.call('psetex', KEYS[1], ARGV[2], ARGV[1] .. ':' .. token)
|
|
5
|
+
return token
|
|
6
|
+
end
|
|
7
|
+
return 0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
VERIFY = """
|
|
11
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
12
|
+
return 1
|
|
13
|
+
end
|
|
14
|
+
return 0
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
RENEW = """
|
|
18
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
19
|
+
redis.call('pexpire', KEYS[1], ARGV[2])
|
|
20
|
+
return 1
|
|
21
|
+
end
|
|
22
|
+
return 0
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
LOAD = """
|
|
26
|
+
if redis.call('get', KEYS[1]) ~= ARGV[1] then
|
|
27
|
+
return {0}
|
|
28
|
+
end
|
|
29
|
+
local payload = redis.call('hget', KEYS[2], 'payload')
|
|
30
|
+
if payload then
|
|
31
|
+
redis.call('pexpire', KEYS[2], ARGV[2])
|
|
32
|
+
return {1, payload}
|
|
33
|
+
end
|
|
34
|
+
return {1}
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
SAVE = """
|
|
38
|
+
if redis.call('get', KEYS[1]) ~= ARGV[1] then
|
|
39
|
+
return 0
|
|
40
|
+
end
|
|
41
|
+
local current = tonumber(redis.call('hget', KEYS[2], 'version') or '0')
|
|
42
|
+
if current ~= tonumber(ARGV[2]) then
|
|
43
|
+
return -1
|
|
44
|
+
end
|
|
45
|
+
redis.call('hset', KEYS[2], 'version', ARGV[3], 'payload', ARGV[4])
|
|
46
|
+
redis.call('pexpire', KEYS[2], ARGV[5])
|
|
47
|
+
return 1
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
RESET = """
|
|
51
|
+
if redis.call('get', KEYS[1]) ~= ARGV[1] then
|
|
52
|
+
return 0
|
|
53
|
+
end
|
|
54
|
+
redis.call('del', KEYS[2])
|
|
55
|
+
return 1
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
RELEASE = """
|
|
59
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
60
|
+
redis.call('del', KEYS[1])
|
|
61
|
+
return 1
|
|
62
|
+
end
|
|
63
|
+
return 0
|
|
64
|
+
"""
|
|
65
|
+
|