edda-framework 0.10.0__py3-none-any.whl → 0.12.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.
- edda/app.py +203 -35
- edda/channels.py +57 -12
- edda/context.py +24 -0
- edda/integrations/mirascope/__init__.py +78 -0
- edda/integrations/mirascope/agent.py +467 -0
- edda/integrations/mirascope/call.py +166 -0
- edda/integrations/mirascope/decorator.py +163 -0
- edda/integrations/mirascope/types.py +268 -0
- edda/migrations/mysql/20251217000000_initial_schema.sql +284 -0
- edda/migrations/postgresql/20251217000000_initial_schema.sql +284 -0
- edda/migrations/sqlite/20251217000000_initial_schema.sql +284 -0
- edda/outbox/relayer.py +34 -7
- edda/storage/migrations.py +435 -0
- edda/storage/models.py +2 -0
- edda/storage/pg_notify.py +5 -8
- edda/storage/sqlalchemy_storage.py +97 -61
- {edda_framework-0.10.0.dist-info → edda_framework-0.12.0.dist-info}/METADATA +47 -3
- {edda_framework-0.10.0.dist-info → edda_framework-0.12.0.dist-info}/RECORD +21 -12
- {edda_framework-0.10.0.dist-info → edda_framework-0.12.0.dist-info}/WHEEL +0 -0
- {edda_framework-0.10.0.dist-info → edda_framework-0.12.0.dist-info}/entry_points.txt +0 -0
- {edda_framework-0.10.0.dist-info → edda_framework-0.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automatic migration support for dbmate-style SQL files.
|
|
3
|
+
|
|
4
|
+
This module provides functions to automatically apply dbmate migration files
|
|
5
|
+
at application startup, eliminating the need to manually run `dbmate up`.
|
|
6
|
+
|
|
7
|
+
The migration system is compatible with dbmate:
|
|
8
|
+
- Uses the same `schema_migrations` table for tracking applied migrations
|
|
9
|
+
- Reads the same `-- migrate:up` / `-- migrate:down` SQL format
|
|
10
|
+
- Supports SQLite, PostgreSQL, and MySQL
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
from collections.abc import AsyncIterator
|
|
19
|
+
from contextlib import asynccontextmanager
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from sqlalchemy import text
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Advisory lock key for migration (consistent across workers)
|
|
31
|
+
MIGRATION_LOCK_KEY = 20251217000000 # Using migration timestamp as lock key
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def detect_db_type(engine: AsyncEngine) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Detect database type from engine URL.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
engine: SQLAlchemy async engine
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
One of 'sqlite', 'postgresql', 'mysql'
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If database type cannot be detected
|
|
46
|
+
"""
|
|
47
|
+
url = str(engine.url)
|
|
48
|
+
|
|
49
|
+
if "sqlite" in url:
|
|
50
|
+
return "sqlite"
|
|
51
|
+
elif "postgresql" in url or "postgres" in url:
|
|
52
|
+
return "postgresql"
|
|
53
|
+
elif "mysql" in url:
|
|
54
|
+
return "mysql"
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f"Cannot detect database type from URL: {url}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_migrations_dir() -> Path | None:
|
|
60
|
+
"""
|
|
61
|
+
Auto-detect migrations directory.
|
|
62
|
+
|
|
63
|
+
Searches in the following order:
|
|
64
|
+
1. Package-bundled migrations (edda/migrations/)
|
|
65
|
+
2. Development environment (schema/db/migrations/)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Path to migrations directory or None if not found
|
|
69
|
+
"""
|
|
70
|
+
# 1. Package-bundled migrations
|
|
71
|
+
pkg_dir = Path(__file__).parent.parent / "migrations"
|
|
72
|
+
if pkg_dir.exists():
|
|
73
|
+
return pkg_dir
|
|
74
|
+
|
|
75
|
+
# 2. Development environment (schema submodule)
|
|
76
|
+
schema_dir = Path.cwd() / "schema" / "db" / "migrations"
|
|
77
|
+
if schema_dir.exists():
|
|
78
|
+
return schema_dir
|
|
79
|
+
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def ensure_schema_migrations_table(engine: AsyncEngine) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Create schema_migrations table if it doesn't exist.
|
|
86
|
+
|
|
87
|
+
This table is compatible with dbmate's migration tracking.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
engine: SQLAlchemy async engine
|
|
91
|
+
"""
|
|
92
|
+
db_type = detect_db_type(engine)
|
|
93
|
+
|
|
94
|
+
if db_type == "mysql":
|
|
95
|
+
# MySQL uses VARCHAR with explicit length
|
|
96
|
+
create_sql = """
|
|
97
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
98
|
+
version VARCHAR(255) PRIMARY KEY
|
|
99
|
+
)
|
|
100
|
+
"""
|
|
101
|
+
else:
|
|
102
|
+
# SQLite and PostgreSQL
|
|
103
|
+
create_sql = """
|
|
104
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
105
|
+
version VARCHAR(255) PRIMARY KEY
|
|
106
|
+
)
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
async with engine.begin() as conn:
|
|
111
|
+
await conn.execute(text(create_sql))
|
|
112
|
+
except Exception as e:
|
|
113
|
+
err_msg = str(e).lower()
|
|
114
|
+
# Handle concurrent table creation - another worker might have created it
|
|
115
|
+
if "already exists" in err_msg or "duplicate" in err_msg or "42p07" in err_msg:
|
|
116
|
+
return
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def get_applied_migrations(engine: AsyncEngine) -> set[str]:
|
|
121
|
+
"""
|
|
122
|
+
Get set of already applied migration versions.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
engine: SQLAlchemy async engine
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Set of applied migration version strings (e.g., "20251217000000")
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
async with engine.connect() as conn:
|
|
132
|
+
result = await conn.execute(text("SELECT version FROM schema_migrations"))
|
|
133
|
+
return {row[0] for row in result.fetchall()}
|
|
134
|
+
except Exception:
|
|
135
|
+
# Table might not exist yet
|
|
136
|
+
return set()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def record_migration(engine: AsyncEngine, version: str) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Record a migration as applied.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
engine: SQLAlchemy async engine
|
|
145
|
+
version: Migration version string (e.g., "20251217000000")
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if recorded successfully, False if already recorded (race condition)
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
async with engine.begin() as conn:
|
|
152
|
+
await conn.execute(
|
|
153
|
+
text("INSERT INTO schema_migrations (version) VALUES (:version)"),
|
|
154
|
+
{"version": version},
|
|
155
|
+
)
|
|
156
|
+
return True
|
|
157
|
+
except Exception as e:
|
|
158
|
+
# Handle race condition: another worker already recorded this migration
|
|
159
|
+
err_msg = str(e).lower()
|
|
160
|
+
if "unique constraint" in err_msg or "duplicate" in err_msg:
|
|
161
|
+
return False
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@asynccontextmanager
|
|
166
|
+
async def migration_lock(engine: AsyncEngine, db_type: str) -> AsyncIterator[bool]:
|
|
167
|
+
"""
|
|
168
|
+
Acquire an advisory lock for migrations.
|
|
169
|
+
|
|
170
|
+
Uses database-specific advisory locks to ensure only one worker
|
|
171
|
+
applies migrations at a time.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
engine: SQLAlchemy async engine
|
|
175
|
+
db_type: Database type ('sqlite', 'postgresql', 'mysql')
|
|
176
|
+
|
|
177
|
+
Yields:
|
|
178
|
+
True if lock was acquired, False otherwise
|
|
179
|
+
"""
|
|
180
|
+
if db_type == "sqlite":
|
|
181
|
+
# SQLite doesn't support advisory locks, but it's single-writer anyway
|
|
182
|
+
yield True
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
acquired = False
|
|
186
|
+
try:
|
|
187
|
+
async with engine.connect() as conn:
|
|
188
|
+
if db_type == "postgresql":
|
|
189
|
+
# Try to acquire advisory lock (non-blocking)
|
|
190
|
+
result = await conn.execute(
|
|
191
|
+
text(f"SELECT pg_try_advisory_lock({MIGRATION_LOCK_KEY})")
|
|
192
|
+
)
|
|
193
|
+
acquired = result.scalar() is True
|
|
194
|
+
elif db_type == "mysql":
|
|
195
|
+
# Try to acquire named lock (0 = no wait)
|
|
196
|
+
result = await conn.execute(text("SELECT GET_LOCK('edda_migration', 0)"))
|
|
197
|
+
acquired = result.scalar() == 1
|
|
198
|
+
|
|
199
|
+
if acquired:
|
|
200
|
+
yield True
|
|
201
|
+
else:
|
|
202
|
+
logger.debug("Could not acquire migration lock, another worker is migrating")
|
|
203
|
+
yield False
|
|
204
|
+
|
|
205
|
+
finally:
|
|
206
|
+
if acquired:
|
|
207
|
+
try:
|
|
208
|
+
async with engine.connect() as conn:
|
|
209
|
+
if db_type == "postgresql":
|
|
210
|
+
await conn.execute(text(f"SELECT pg_advisory_unlock({MIGRATION_LOCK_KEY})"))
|
|
211
|
+
await conn.commit()
|
|
212
|
+
elif db_type == "mysql":
|
|
213
|
+
await conn.execute(text("SELECT RELEASE_LOCK('edda_migration')"))
|
|
214
|
+
await conn.commit()
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.warning(f"Failed to release migration lock: {e}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def extract_version_from_filename(filename: str) -> str:
|
|
220
|
+
"""
|
|
221
|
+
Extract version from migration filename.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
filename: Migration filename (e.g., "20251217000000_initial_schema.sql")
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Version string (e.g., "20251217000000")
|
|
228
|
+
"""
|
|
229
|
+
# dbmate uses format: YYYYMMDDHHMMSS_description.sql
|
|
230
|
+
match = re.match(r"^(\d+)_", filename)
|
|
231
|
+
if match:
|
|
232
|
+
return match.group(1)
|
|
233
|
+
return filename.replace(".sql", "")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def parse_migration_file(content: str) -> tuple[str, str]:
|
|
237
|
+
"""
|
|
238
|
+
Parse dbmate migration file content.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
content: Full content of migration SQL file
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Tuple of (up_sql, down_sql)
|
|
245
|
+
"""
|
|
246
|
+
# Extract "-- migrate:up" section
|
|
247
|
+
up_match = re.search(
|
|
248
|
+
r"-- migrate:up\s*(.*?)(?:-- migrate:down|$)",
|
|
249
|
+
content,
|
|
250
|
+
re.DOTALL,
|
|
251
|
+
)
|
|
252
|
+
up_sql = up_match.group(1).strip() if up_match else ""
|
|
253
|
+
|
|
254
|
+
# Extract "-- migrate:down" section
|
|
255
|
+
down_match = re.search(
|
|
256
|
+
r"-- migrate:down\s*(.*?)$",
|
|
257
|
+
content,
|
|
258
|
+
re.DOTALL,
|
|
259
|
+
)
|
|
260
|
+
down_sql = down_match.group(1).strip() if down_match else ""
|
|
261
|
+
|
|
262
|
+
return up_sql, down_sql
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def execute_sql_statements(
|
|
266
|
+
engine: AsyncEngine,
|
|
267
|
+
sql: str,
|
|
268
|
+
db_type: str,
|
|
269
|
+
) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Execute SQL statements from migration file.
|
|
272
|
+
|
|
273
|
+
Handles:
|
|
274
|
+
- Multiple statements separated by semicolons
|
|
275
|
+
- Comment lines
|
|
276
|
+
- MySQL DELIMITER blocks (simplified handling)
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
engine: SQLAlchemy async engine
|
|
280
|
+
sql: SQL string containing one or more statements
|
|
281
|
+
db_type: Database type ('sqlite', 'postgresql', 'mysql')
|
|
282
|
+
"""
|
|
283
|
+
if not sql:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# For MySQL, handle DELIMITER and stored procedures specially
|
|
287
|
+
if db_type == "mysql" and "DELIMITER" in sql:
|
|
288
|
+
# Skip procedure-based migrations for now, execute simpler parts
|
|
289
|
+
sql = re.sub(
|
|
290
|
+
r"DROP PROCEDURE.*?;|DELIMITER.*?DELIMITER ;",
|
|
291
|
+
"",
|
|
292
|
+
sql,
|
|
293
|
+
flags=re.DOTALL,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Split by semicolons and execute each statement
|
|
297
|
+
statements = [s.strip() for s in sql.split(";") if s.strip()]
|
|
298
|
+
|
|
299
|
+
async with engine.begin() as conn:
|
|
300
|
+
for stmt in statements:
|
|
301
|
+
if not stmt:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
# Strip leading comment lines to get actual SQL
|
|
305
|
+
lines = stmt.split("\n")
|
|
306
|
+
sql_lines = []
|
|
307
|
+
for line in lines:
|
|
308
|
+
stripped = line.strip()
|
|
309
|
+
# Skip empty lines and comment-only lines
|
|
310
|
+
if not stripped or stripped.startswith("--"):
|
|
311
|
+
continue
|
|
312
|
+
sql_lines.append(line)
|
|
313
|
+
|
|
314
|
+
actual_sql = "\n".join(sql_lines).strip()
|
|
315
|
+
if not actual_sql:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
await conn.execute(text(actual_sql))
|
|
320
|
+
except Exception as e:
|
|
321
|
+
# Log but continue - some statements might fail if objects exist
|
|
322
|
+
err_msg = str(e).lower()
|
|
323
|
+
if "already exists" not in err_msg and "duplicate" not in err_msg:
|
|
324
|
+
raise
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
async def apply_dbmate_migrations(
|
|
328
|
+
engine: AsyncEngine,
|
|
329
|
+
migrations_dir: Path | None = None,
|
|
330
|
+
) -> list[str]:
|
|
331
|
+
"""
|
|
332
|
+
Apply dbmate migration files automatically.
|
|
333
|
+
|
|
334
|
+
This function:
|
|
335
|
+
1. Detects database type from engine URL
|
|
336
|
+
2. Finds migration files for that database type
|
|
337
|
+
3. Checks which migrations have already been applied
|
|
338
|
+
4. Applies pending migrations in order
|
|
339
|
+
5. Records applied migrations in schema_migrations table
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
engine: SQLAlchemy async engine
|
|
343
|
+
migrations_dir: Base migrations directory (contains sqlite/, postgresql/, mysql/ subdirs).
|
|
344
|
+
If None, auto-detects from package or schema/ submodule.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
List of applied migration versions
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
FileNotFoundError: If migrations directory not found
|
|
351
|
+
ValueError: If database type cannot be detected
|
|
352
|
+
"""
|
|
353
|
+
# Find migrations directory
|
|
354
|
+
if migrations_dir is None:
|
|
355
|
+
migrations_dir = find_migrations_dir()
|
|
356
|
+
|
|
357
|
+
if migrations_dir is None:
|
|
358
|
+
logger.warning(
|
|
359
|
+
"No migrations directory found. "
|
|
360
|
+
"Skipping automatic migration. "
|
|
361
|
+
"Set migrations_dir parameter or use auto_migrate=False."
|
|
362
|
+
)
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
# Detect database type
|
|
366
|
+
db_type = detect_db_type(engine)
|
|
367
|
+
db_migrations_dir = migrations_dir / db_type
|
|
368
|
+
|
|
369
|
+
if not db_migrations_dir.exists():
|
|
370
|
+
logger.warning(
|
|
371
|
+
f"Migrations directory for {db_type} not found: {db_migrations_dir}. "
|
|
372
|
+
"Skipping automatic migration."
|
|
373
|
+
)
|
|
374
|
+
return []
|
|
375
|
+
|
|
376
|
+
# Ensure schema_migrations table exists
|
|
377
|
+
await ensure_schema_migrations_table(engine)
|
|
378
|
+
|
|
379
|
+
# Use advisory lock to ensure only one worker applies migrations
|
|
380
|
+
async with migration_lock(engine, db_type) as acquired:
|
|
381
|
+
if not acquired:
|
|
382
|
+
# Another worker is applying migrations, wait and check applied
|
|
383
|
+
logger.info("Another worker is applying migrations, waiting...")
|
|
384
|
+
# Wait a bit and return - migrations will be applied by the other worker
|
|
385
|
+
await asyncio.sleep(1)
|
|
386
|
+
return []
|
|
387
|
+
|
|
388
|
+
# Get already applied migrations (inside lock to avoid race)
|
|
389
|
+
applied = await get_applied_migrations(engine)
|
|
390
|
+
|
|
391
|
+
# Get all migration files sorted by name (timestamp order)
|
|
392
|
+
migration_files = sorted(db_migrations_dir.glob("*.sql"))
|
|
393
|
+
|
|
394
|
+
applied_versions: list[str] = []
|
|
395
|
+
|
|
396
|
+
for migration_file in migration_files:
|
|
397
|
+
version = extract_version_from_filename(migration_file.name)
|
|
398
|
+
|
|
399
|
+
# Skip if already applied
|
|
400
|
+
if version in applied:
|
|
401
|
+
logger.debug(f"Migration {version} already applied, skipping")
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
logger.info(f"Applying migration: {migration_file.name}")
|
|
405
|
+
|
|
406
|
+
# Read and parse migration file
|
|
407
|
+
content = migration_file.read_text()
|
|
408
|
+
up_sql, _ = parse_migration_file(content)
|
|
409
|
+
|
|
410
|
+
if not up_sql:
|
|
411
|
+
logger.warning(f"No '-- migrate:up' section found in {migration_file.name}")
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
# Execute migration
|
|
415
|
+
try:
|
|
416
|
+
await execute_sql_statements(engine, up_sql, db_type)
|
|
417
|
+
|
|
418
|
+
# Record as applied (handles race condition with other workers)
|
|
419
|
+
recorded = await record_migration(engine, version)
|
|
420
|
+
if recorded:
|
|
421
|
+
applied_versions.append(version)
|
|
422
|
+
logger.info(f"Successfully applied migration: {version}")
|
|
423
|
+
else:
|
|
424
|
+
# Another worker already applied this migration
|
|
425
|
+
logger.debug(f"Migration {version} was applied by another worker")
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.error(f"Failed to apply migration {version}: {e}")
|
|
428
|
+
raise
|
|
429
|
+
|
|
430
|
+
if applied_versions:
|
|
431
|
+
logger.info(f"Applied {len(applied_versions)} migration(s)")
|
|
432
|
+
else:
|
|
433
|
+
logger.debug("No pending migrations to apply")
|
|
434
|
+
|
|
435
|
+
return applied_versions
|
edda/storage/models.py
CHANGED
|
@@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS workflow_instances (
|
|
|
29
29
|
workflow_name TEXT NOT NULL,
|
|
30
30
|
source_hash TEXT NOT NULL,
|
|
31
31
|
owner_service TEXT NOT NULL,
|
|
32
|
+
framework TEXT NOT NULL DEFAULT 'python',
|
|
32
33
|
status TEXT NOT NULL DEFAULT 'running',
|
|
33
34
|
current_activity_id TEXT,
|
|
34
35
|
continued_from TEXT,
|
|
@@ -52,6 +53,7 @@ WORKFLOW_INSTANCES_INDEXES = [
|
|
|
52
53
|
"CREATE INDEX IF NOT EXISTS idx_instances_status ON workflow_instances(status);",
|
|
53
54
|
"CREATE INDEX IF NOT EXISTS idx_instances_workflow ON workflow_instances(workflow_name);",
|
|
54
55
|
"CREATE INDEX IF NOT EXISTS idx_instances_owner ON workflow_instances(owner_service);",
|
|
56
|
+
"CREATE INDEX IF NOT EXISTS idx_instances_framework ON workflow_instances(framework);",
|
|
55
57
|
"CREATE INDEX IF NOT EXISTS idx_instances_locked ON workflow_instances(locked_by, locked_at);",
|
|
56
58
|
"CREATE INDEX IF NOT EXISTS idx_instances_updated ON workflow_instances(updated_at);",
|
|
57
59
|
"CREATE INDEX IF NOT EXISTS idx_instances_hash ON workflow_instances(source_hash);",
|
edda/storage/pg_notify.py
CHANGED
|
@@ -288,21 +288,18 @@ class PostgresNotifyListener:
|
|
|
288
288
|
await asyncio.sleep(self._reconnect_interval)
|
|
289
289
|
|
|
290
290
|
|
|
291
|
-
def get_notify_channel_for_message(
|
|
291
|
+
def get_notify_channel_for_message(_channel: str) -> str:
|
|
292
292
|
"""Convert Edda channel name to PostgreSQL NOTIFY channel.
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
Returns a unified channel name that both Python and Go frameworks use.
|
|
295
295
|
|
|
296
296
|
Args:
|
|
297
|
-
|
|
297
|
+
_channel: The Edda channel name (unused, kept for API compatibility).
|
|
298
298
|
|
|
299
299
|
Returns:
|
|
300
|
-
PostgreSQL
|
|
300
|
+
Unified PostgreSQL channel name.
|
|
301
301
|
"""
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
h = hashlib.sha256(channel.encode()).hexdigest()[:16]
|
|
305
|
-
return f"edda_msg_{h}"
|
|
302
|
+
return "workflow_channel_message"
|
|
306
303
|
|
|
307
304
|
|
|
308
305
|
def make_notify_payload(data: dict[str, Any]) -> str:
|