sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Script to add the 10 UMP innovation ideas to the vector database
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.infrastructure.vector_db.qdrant_service import get_qdrant_service
|
||||
|
||||
UMP_IDEAS = [
|
||||
{
|
||||
"title": "Nền tảng Trợ lý AI học tập lâm sàng (Clinical AI Tutor)",
|
||||
"description": "Ứng dụng AI đóng vai trò trợ giảng cho sinh viên y, hỗ trợ phân tích ca bệnh giả lập, giải thích cận lâm sàng, và gợi ý chẩn đoán theo phác đồ Việt Nam.",
|
||||
"category": "Giáo dục - AI"
|
||||
},
|
||||
{
|
||||
"title": "Hệ thống bệnh án điện tử học thuật (Academic EMR Sandbox)",
|
||||
"description": "Môi trường EMR mô phỏng cho đào tạo và nghiên cứu, cho phép sinh viên và giảng viên thực hành nhập – phân tích – khai thác dữ liệu y khoa mà không ảnh hưởng dữ liệu bệnh nhân thật.",
|
||||
"category": "Giáo dục - Chuyển đổi số"
|
||||
},
|
||||
{
|
||||
"title": "Trung tâm mô phỏng y khoa bằng AR/VR & Digital Twin",
|
||||
"description": "Xây dựng phòng lab mô phỏng phẫu thuật, cấp cứu, và quy trình điều trị bằng AR/VR, kết hợp mô hình \"digital twin\" của cơ thể người phục vụ đào tạo nâng cao.",
|
||||
"category": "Giáo dục - AR/VR"
|
||||
},
|
||||
{
|
||||
"title": "Chương trình Y tế cộng đồng số cho vùng sâu vùng xa",
|
||||
"description": "Kết hợp telehealth, trợ lý ảo y tế (agentic care) và AI sàng lọc sớm bệnh không lây (NCD) cho người dân vùng nông thôn, miền núi và hải đảo.",
|
||||
"category": "Tác động xã hội - Telehealth"
|
||||
},
|
||||
{
|
||||
"title": "Nền tảng nghiên cứu AI y sinh dùng chung (UMP AI Research Hub)",
|
||||
"description": "Cung cấp hạ tầng GPU, kho dữ liệu y khoa ẩn danh, và công cụ phân tích AI cho giảng viên – nghiên cứu sinh – startup hợp tác nghiên cứu.",
|
||||
"category": "Nghiên cứu - AI"
|
||||
},
|
||||
{
|
||||
"title": "Hệ thống theo dõi và dự báo sức khỏe sinh viên & nhân viên y tế",
|
||||
"description": "Ứng dụng phân tích dữ liệu và AI để phát hiện sớm stress, burnout, và vấn đề sức khỏe tâm thần trong cộng đồng sinh viên và nhân viên y tế.",
|
||||
"category": "Tác động xã hội - Sức khỏe"
|
||||
},
|
||||
{
|
||||
"title": "Vườn ươm khởi nghiệp công nghệ y sinh (MedTech Incubator)",
|
||||
"description": "Hỗ trợ sinh viên, bác sĩ và giảng viên phát triển startup MedTech, HealthTech, AI y tế thông qua mentoring, quỹ seed và kết nối bệnh viện – doanh nghiệp.",
|
||||
"category": "Khởi nghiệp - MedTech"
|
||||
},
|
||||
{
|
||||
"title": "Hệ thống quản lý chất lượng đào tạo và kiểm định số",
|
||||
"description": "Số hóa toàn bộ quy trình đảm bảo chất lượng nội bộ (IQA), đánh giá chương trình đào tạo, và chuẩn hóa theo tiêu chuẩn quốc tế (WFME, AUN-QA).",
|
||||
"category": "Giáo dục - Quản lý chất lượng"
|
||||
},
|
||||
{
|
||||
"title": "Nền tảng dữ liệu lớn phòng chống dịch và bệnh không lây",
|
||||
"description": "Phân tích dữ liệu dịch tễ, môi trường, và hành vi để dự báo dịch bệnh, hỗ trợ Sở Y tế và Bộ Y tế trong ra quyết định chính sách.",
|
||||
"category": "Nghiên cứu - Dịch tễ học"
|
||||
},
|
||||
{
|
||||
"title": "Học viện Y học chính xác & Y học cá thể hóa",
|
||||
"description": "Kết hợp dữ liệu gen, hình ảnh y khoa, lối sống và AI để nghiên cứu và ứng dụng điều trị cá thể hóa cho bệnh ung thư, tim mạch và bệnh mạn tính.",
|
||||
"category": "Nghiên cứu - Y học chính xác"
|
||||
}
|
||||
]
|
||||
|
||||
async def main():
|
||||
"""Add all UMP ideas to the database"""
|
||||
print("Initializing Qdrant service...")
|
||||
qdrant_service = get_qdrant_service()
|
||||
|
||||
print("Initializing collection...")
|
||||
await qdrant_service.initialize_collection()
|
||||
|
||||
print(f"Adding {len(UMP_IDEAS)} ideas to the database...")
|
||||
results = []
|
||||
for i, idea in enumerate(UMP_IDEAS, 1):
|
||||
try:
|
||||
print(f"Adding idea {i}/{len(UMP_IDEAS)}: {idea['title']}")
|
||||
result = await qdrant_service.add_idea(
|
||||
title=idea['title'],
|
||||
description=idea['description'],
|
||||
category=idea['category']
|
||||
)
|
||||
results.append(result)
|
||||
print(f"✓ Added: {result['id']}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error adding idea {i}: {e}")
|
||||
|
||||
print(f"\n✓ Successfully added {len(results)}/{len(UMP_IDEAS)} ideas")
|
||||
return results
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apply migration 007 (user_roles.admin_from_email_policy) to an EXISTING Postgres.
|
||||
# initdb scripts in docker-entrypoint-initdb.d run only on first volume creation.
|
||||
#
|
||||
# Default (full SQL file): adds column, runs one-time policy DELETE/UPDATE (see
|
||||
# be0/migrations/007_user_roles_email_policy_admin.sql before running on prod).
|
||||
#
|
||||
# Usage (from anywhere):
|
||||
# ./be0/scripts/apply-migration-007.sh
|
||||
# ./be0/scripts/apply-migration-007.sh --schema-only # only ADD COLUMN (safest repeat)
|
||||
#
|
||||
# On a remote host (SSH to be0/docker host, repo or copy of migrations present):
|
||||
# export POSTGRES_CONTAINER=initiative-postgres POSTGRES_USER=initiative POSTGRES_DB=initiatives
|
||||
# ./be0/scripts/apply-migration-007.sh
|
||||
#
|
||||
# From repo root (wrapper):
|
||||
# ./scripts/apply-migration-007-postgres.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCHEMA_ONLY=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--schema-only) SCHEMA_ONLY=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,20p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BE0_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SQL_FULL="$BE0_ROOT/migrations/007_user_roles_email_policy_admin.sql"
|
||||
CONTAINER="${POSTGRES_CONTAINER:-initiative-postgres}"
|
||||
PGUSER="${POSTGRES_USER:-initiative}"
|
||||
PGDATABASE="${POSTGRES_DB:-initiatives}"
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "error: Docker is not reachable (is the daemon running?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then
|
||||
echo "error: container not found: $CONTAINER (set POSTGRES_CONTAINER)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo false)" != "true" ]]; then
|
||||
echo "error: container is not running: $CONTAINER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
apply_schema_only() {
|
||||
docker exec -i "$CONTAINER" psql -U "$PGUSER" -d "$PGDATABASE" -v ON_ERROR_STOP=1 <<'SQL'
|
||||
ALTER TABLE user_roles ADD COLUMN IF NOT EXISTS admin_from_email_policy BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
COMMENT ON COLUMN user_roles.admin_from_email_policy IS
|
||||
'TRUE when admin was granted by email allow-list (AUTH_ADMIN_EMAILS). Reconciliation may DELETE this row if the user email is no longer in the list. FALSE preserves manually granted admin (future / exceptional).';
|
||||
SQL
|
||||
}
|
||||
|
||||
apply_full() {
|
||||
if [[ ! -f "$SQL_FULL" ]]; then
|
||||
echo "error: missing migration file: $SQL_FULL" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker exec -i "$CONTAINER" psql -U "$PGUSER" -d "$PGDATABASE" -v ON_ERROR_STOP=1 <"$SQL_FULL"
|
||||
}
|
||||
|
||||
verify_column() {
|
||||
local out
|
||||
out="$(docker exec "$CONTAINER" psql -U "$PGUSER" -d "$PGDATABASE" -tAc \
|
||||
"SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'user_roles' AND column_name = 'admin_from_email_policy'")"
|
||||
if [[ "${out//$'\r'/}" != "1" ]]; then
|
||||
echo "error: verification failed: column admin_from_email_policy missing on public.user_roles" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if (( SCHEMA_ONLY )); then
|
||||
echo "Applying schema only (ADD COLUMN + COMMENT) → $CONTAINER / $PGDATABASE"
|
||||
apply_schema_only
|
||||
else
|
||||
echo "Applying full 007_user_roles_email_policy_admin.sql → $CONTAINER / $PGDATABASE"
|
||||
apply_full
|
||||
fi
|
||||
|
||||
verify_column
|
||||
echo "ok: user_roles.admin_from_email_policy is present; admin register/login should work with current be0."
|
||||
@@ -0,0 +1,533 @@
|
||||
"""
|
||||
Apply idempotent SQL fixes when the DB volume predates newer migrations.
|
||||
|
||||
- ``008_audit_events.sql`` when ``audit_events`` is missing (older volumes never
|
||||
ran ``docker-entrypoint-initdb.d`` for new files).
|
||||
- ``009_backup_artifact_roles_storage_kind.sql`` when ``storage_kind`` is missing.
|
||||
- ``010_user_staff_profiles.sql`` + ``011_academic_titles_vn.sql`` when
|
||||
``academic_titles`` is missing (staff profile / register flow).
|
||||
- ``013_email_verification.sql`` when ``email_verification_tokens`` is missing.
|
||||
- ``014_registration_otp.sql`` when ``registration_otp_codes`` is missing.
|
||||
|
||||
Run automatically from entrypoint when ``INITIATIVE_DATABASE_URL`` is set.
|
||||
Standalone:
|
||||
|
||||
INITIATIVE_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname \\
|
||||
python scripts/apply_initiative_migrations.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _async_url_to_asyncpg_dsn(url: str) -> str:
|
||||
u = url.strip()
|
||||
if "+asyncpg" in u:
|
||||
u = u.replace("postgresql+asyncpg://", "postgresql://", 1)
|
||||
return u
|
||||
|
||||
|
||||
def _strip_sql_comments(text: str) -> str:
|
||||
lines: list[str] = []
|
||||
for line in text.splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith("--"):
|
||||
continue
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _split_sql_statements(text: str) -> list[str]:
|
||||
"""Split on semicolons outside ``$$`` dollar-quoted blocks (008 uses ``DO $$``)."""
|
||||
statements: list[str] = []
|
||||
buf: list[str] = []
|
||||
i = 0
|
||||
n = len(text)
|
||||
in_dollar = False
|
||||
while i < n:
|
||||
if text.startswith("$$", i):
|
||||
in_dollar = not in_dollar
|
||||
buf.append("$$")
|
||||
i += 2
|
||||
continue
|
||||
ch = text[i]
|
||||
if ch == ";" and not in_dollar:
|
||||
stmt = "".join(buf).strip()
|
||||
if stmt:
|
||||
statements.append(stmt)
|
||||
buf = []
|
||||
i += 1
|
||||
continue
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
tail = "".join(buf).strip()
|
||||
if tail:
|
||||
statements.append(tail)
|
||||
return statements
|
||||
|
||||
|
||||
async def _needs_audit_events_migration(conn) -> bool:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'audit_events'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_backup_migration(conn) -> bool:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'application_artifacts'
|
||||
AND column_name = 'storage_kind'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_staff_profiles_migration(conn) -> bool:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'academic_titles'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_email_verification_migration(conn) -> bool:
|
||||
"""True when verification tokens table is missing (013 also adds users.email_verified)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'email_verification_tokens'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_registration_otp_migration(conn) -> bool:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'registration_otp_codes'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_document_templates_migration(conn) -> bool:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'document_templates'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_research_projects_migration(conn) -> bool:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'research_projects'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_datasets_migration(conn) -> bool:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_datasets'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_segmentation_columns_migration(conn) -> bool:
|
||||
"""True when imagehub_dataset_files lacks the segmentation-link columns (018)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_dataset_files'
|
||||
AND column_name = 'file_kind'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_cloud_import_migration(conn) -> bool:
|
||||
"""True when the cloud-import storage_methods table is absent (019)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_storage_methods'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_stages_migration(conn) -> bool:
|
||||
"""True when the dataset-stages table is absent (020)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_dataset_stages'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_tasks_migration(conn) -> bool:
|
||||
"""True when the per-file task-pipeline table is absent (021)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_tasks'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_task_annotations_migration(conn) -> bool:
|
||||
"""True when imagehub_tasks lacks the annotations column (022)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_tasks'
|
||||
AND column_name = 'annotations'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_members_migration(conn) -> bool:
|
||||
"""True when the dataset-membership table is absent (023)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_dataset_members'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_dataset_project_link_migration(conn) -> bool:
|
||||
"""True when imagehub_datasets.research_project_id is absent (024)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_datasets'
|
||||
AND column_name = 'research_project_id'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_review_events_migration(conn) -> bool:
|
||||
"""True when the task-review-events table is absent (025)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_task_review_events'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_folder_path_migration(conn) -> bool:
|
||||
"""True when imagehub_dataset_files.folder_path is absent (026)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_dataset_files'
|
||||
AND column_name = 'folder_path'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _needs_imagehub_label_map_migration(conn) -> bool:
|
||||
"""True when imagehub_datasets.label_map is absent (027)."""
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'imagehub_datasets'
|
||||
AND column_name = 'label_map'
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return row is None
|
||||
|
||||
|
||||
async def _apply_sql_file(conn, path: Path, label: str) -> None:
|
||||
body = _strip_sql_comments(path.read_text(encoding="utf-8"))
|
||||
for stmt in _split_sql_statements(body):
|
||||
await conn.execute(stmt)
|
||||
print(f"apply_initiative_migrations: {label} applied.")
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
raw_url = (os.environ.get("INITIATIVE_DATABASE_URL") or "").strip()
|
||||
if not raw_url.lower().startswith("postgresql"):
|
||||
print("apply_initiative_migrations: no PostgreSQL URL; skipping.", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
m008 = root / "migrations" / "008_audit_events.sql"
|
||||
m009 = root / "migrations" / "009_backup_artifact_roles_storage_kind.sql"
|
||||
m010 = root / "migrations" / "010_user_staff_profiles.sql"
|
||||
m011 = root / "migrations" / "011_academic_titles_vn.sql"
|
||||
for p in (m008, m009, m010, m011):
|
||||
if not p.is_file():
|
||||
print(f"apply_initiative_migrations: missing {p}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
import asyncpg
|
||||
|
||||
dsn = _async_url_to_asyncpg_dsn(raw_url)
|
||||
conn = await asyncpg.connect(dsn, timeout=60)
|
||||
try:
|
||||
if await _needs_audit_events_migration(conn):
|
||||
print("apply_initiative_migrations: applying 008_audit_events …")
|
||||
await _apply_sql_file(conn, m008, "008_audit_events")
|
||||
else:
|
||||
print("apply_initiative_migrations: audit_events present; OK.")
|
||||
|
||||
if await _needs_backup_migration(conn):
|
||||
print("apply_initiative_migrations: applying 009_backup_artifact_roles_storage_kind …")
|
||||
await _apply_sql_file(conn, m009, "009_backup_artifact_roles_storage_kind")
|
||||
else:
|
||||
print("apply_initiative_migrations: application_artifacts.storage_kind present; OK.")
|
||||
|
||||
if await _needs_staff_profiles_migration(conn):
|
||||
print("apply_initiative_migrations: applying 010_user_staff_profiles …")
|
||||
await _apply_sql_file(conn, m010, "010_user_staff_profiles")
|
||||
print("apply_initiative_migrations: applying 011_academic_titles_vn …")
|
||||
await _apply_sql_file(conn, m011, "011_academic_titles_vn")
|
||||
else:
|
||||
print("apply_initiative_migrations: academic_titles present; OK.")
|
||||
|
||||
m013 = root / "migrations" / "013_email_verification.sql"
|
||||
if not m013.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m013}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_email_verification_migration(conn):
|
||||
print("apply_initiative_migrations: applying 013_email_verification …")
|
||||
await _apply_sql_file(conn, m013, "013_email_verification")
|
||||
else:
|
||||
print("apply_initiative_migrations: email_verification_tokens present; OK.")
|
||||
|
||||
m014 = root / "migrations" / "014_registration_otp.sql"
|
||||
if not m014.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m014}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_registration_otp_migration(conn):
|
||||
print("apply_initiative_migrations: applying 014_registration_otp …")
|
||||
await _apply_sql_file(conn, m014, "014_registration_otp")
|
||||
else:
|
||||
print("apply_initiative_migrations: registration_otp_codes present; OK.")
|
||||
|
||||
m015 = root / "migrations" / "015_document_templates.sql"
|
||||
if not m015.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m015}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_document_templates_migration(conn):
|
||||
print("apply_initiative_migrations: applying 015_document_templates …")
|
||||
await _apply_sql_file(conn, m015, "015_document_templates")
|
||||
else:
|
||||
print("apply_initiative_migrations: document_templates present; OK.")
|
||||
|
||||
m016 = root / "migrations" / "016_research_projects.sql"
|
||||
if not m016.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m016}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_research_projects_migration(conn):
|
||||
print("apply_initiative_migrations: applying 016_research_projects …")
|
||||
await _apply_sql_file(conn, m016, "016_research_projects")
|
||||
else:
|
||||
print("apply_initiative_migrations: research_projects present; OK.")
|
||||
|
||||
m017 = root / "migrations" / "017_imagehub_datasets.sql"
|
||||
if not m017.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m017}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_datasets_migration(conn):
|
||||
print("apply_initiative_migrations: applying 017_imagehub_datasets …")
|
||||
await _apply_sql_file(conn, m017, "017_imagehub_datasets")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_datasets present; OK.")
|
||||
|
||||
m018 = root / "migrations" / "018_imagehub_segmentation_links.sql"
|
||||
if not m018.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m018}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_segmentation_columns_migration(conn):
|
||||
print("apply_initiative_migrations: applying 018_imagehub_segmentation_links …")
|
||||
await _apply_sql_file(conn, m018, "018_imagehub_segmentation_links")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_dataset_files.file_kind present; OK.")
|
||||
|
||||
m019 = root / "migrations" / "019_imagehub_cloud_import.sql"
|
||||
if not m019.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m019}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_cloud_import_migration(conn):
|
||||
print("apply_initiative_migrations: applying 019_imagehub_cloud_import …")
|
||||
await _apply_sql_file(conn, m019, "019_imagehub_cloud_import")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_storage_methods present; OK.")
|
||||
|
||||
m020 = root / "migrations" / "020_imagehub_dataset_stages.sql"
|
||||
if not m020.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m020}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_stages_migration(conn):
|
||||
print("apply_initiative_migrations: applying 020_imagehub_dataset_stages …")
|
||||
await _apply_sql_file(conn, m020, "020_imagehub_dataset_stages")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_dataset_stages present; OK.")
|
||||
|
||||
m021 = root / "migrations" / "021_imagehub_task_pipeline.sql"
|
||||
if not m021.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m021}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_tasks_migration(conn):
|
||||
print("apply_initiative_migrations: applying 021_imagehub_task_pipeline …")
|
||||
await _apply_sql_file(conn, m021, "021_imagehub_task_pipeline")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_tasks present; OK.")
|
||||
|
||||
m022 = root / "migrations" / "022_imagehub_task_annotations.sql"
|
||||
if not m022.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m022}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_task_annotations_migration(conn):
|
||||
print("apply_initiative_migrations: applying 022_imagehub_task_annotations …")
|
||||
await _apply_sql_file(conn, m022, "022_imagehub_task_annotations")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_tasks.annotations present; OK.")
|
||||
|
||||
m023 = root / "migrations" / "023_imagehub_dataset_members.sql"
|
||||
if not m023.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m023}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_members_migration(conn):
|
||||
print("apply_initiative_migrations: applying 023_imagehub_dataset_members …")
|
||||
await _apply_sql_file(conn, m023, "023_imagehub_dataset_members")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_dataset_members present; OK.")
|
||||
|
||||
m024 = root / "migrations" / "024_imagehub_dataset_project_link.sql"
|
||||
if not m024.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m024}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_dataset_project_link_migration(conn):
|
||||
print("apply_initiative_migrations: applying 024_imagehub_dataset_project_link …")
|
||||
await _apply_sql_file(conn, m024, "024_imagehub_dataset_project_link")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_datasets.research_project_id present; OK.")
|
||||
|
||||
m025 = root / "migrations" / "025_imagehub_task_review_events.sql"
|
||||
if not m025.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m025}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_review_events_migration(conn):
|
||||
print("apply_initiative_migrations: applying 025_imagehub_task_review_events …")
|
||||
await _apply_sql_file(conn, m025, "025_imagehub_task_review_events")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_task_review_events present; OK.")
|
||||
|
||||
m026 = root / "migrations" / "026_imagehub_file_folder_path.sql"
|
||||
if not m026.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m026}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_folder_path_migration(conn):
|
||||
print("apply_initiative_migrations: applying 026_imagehub_file_folder_path …")
|
||||
await _apply_sql_file(conn, m026, "026_imagehub_file_folder_path")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_dataset_files.folder_path present; OK.")
|
||||
|
||||
m027 = root / "migrations" / "027_imagehub_dataset_label_map.sql"
|
||||
if not m027.is_file():
|
||||
print(f"apply_initiative_migrations: missing {m027}", file=sys.stderr)
|
||||
return 1
|
||||
if await _needs_imagehub_label_map_migration(conn):
|
||||
print("apply_initiative_migrations: applying 027_imagehub_dataset_label_map …")
|
||||
await _apply_sql_file(conn, m027, "027_imagehub_dataset_label_map")
|
||||
else:
|
||||
print("apply_initiative_migrations: imagehub_datasets.label_map present; OK.")
|
||||
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"apply_initiative_migrations: FAILED: {exc}", file=sys.stderr)
|
||||
if os.environ.get("INITIATIVE_DB_STRICT_MIGRATE", "").strip().lower() in ("1", "true", "yes"):
|
||||
return 1
|
||||
return 0
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(asyncio.run(main()))
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI: merge a mis-linked submission onto the real CASE-* initiative row and delete the orphan initiative.
|
||||
|
||||
Usage (dry-run — default, no writes):
|
||||
|
||||
cd be0
|
||||
export INITIATIVE_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/initiatives"
|
||||
python scripts/repair_split_submission.py --submission-id sub-d560fbb6f2944ec6
|
||||
|
||||
Apply (commits one transaction):
|
||||
|
||||
python scripts/repair_split_submission.py --submission-id sub-... --good-case CASE-YOURCODE --execute
|
||||
|
||||
Requires the same Postgres URL as the API (`INITIATIVE_DATABASE_URL` / `DATABASE_URL`).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
|
||||
async def _main_async() -> int:
|
||||
p = argparse.ArgumentParser(description="Repair split submission / wrong initiative linkage.")
|
||||
p.add_argument(
|
||||
"--submission-id",
|
||||
required=True,
|
||||
help="submissionRecord.id (e.g. sub-d560fbb6f2944ec6)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--good-case",
|
||||
dest="good_case",
|
||||
default=None,
|
||||
help="Explicit CASE-* code for the autosave row (recommended if owner has multiple drafts)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Apply changes (otherwise dry-run only)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
os.environ.setdefault("INITIATIVE_DATABASE_URL", os.getenv("DATABASE_URL") or "")
|
||||
from src.initiative_db.engine import get_session, init_engine, is_postgres_enabled
|
||||
from src.initiative_db.repair_split_submission import repair_submission_cross_initiative_merge
|
||||
|
||||
if not is_postgres_enabled():
|
||||
print("Error: set INITIATIVE_DATABASE_URL=postgresql+asyncpg://.../initiatives", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
await init_engine()
|
||||
|
||||
async with get_session() as session:
|
||||
report = await repair_submission_cross_initiative_merge(
|
||||
session,
|
||||
submission_record_id=args.submission_id.strip(),
|
||||
good_case_code_explicit=(args.good_case or "").strip() or None,
|
||||
dry_run=not args.execute,
|
||||
)
|
||||
|
||||
lines = [
|
||||
f"dry_run={report.dry_run}",
|
||||
f"submission_record_id={report.submission_record_id}",
|
||||
f"owner_id={report.owner_id or '(n/a)'}",
|
||||
f"bad_case={report.bad_case_code or '(n/a)'}",
|
||||
f"good_case={report.good_case_code or '(n/a)'}",
|
||||
]
|
||||
if report.skipped:
|
||||
lines.append(f"SKIPPED: {report.skipped}")
|
||||
lines.extend(report.actions)
|
||||
print("\n".join(lines))
|
||||
|
||||
if args.execute and report.skipped:
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> None:
|
||||
raise SystemExit(asyncio.run(_main_async()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user