draft-board 0.1.0-beta.0
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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""add_repos_and_board_repos
|
|
2
|
+
|
|
3
|
+
Revision ID: add_repos_001
|
|
4
|
+
Revises: 357c780ee445
|
|
5
|
+
Create Date: 2026-01-29 22:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
from sqlalchemy.sql import column, table
|
|
15
|
+
|
|
16
|
+
from alembic import op
|
|
17
|
+
|
|
18
|
+
# revision identifiers, used by Alembic.
|
|
19
|
+
revision: str = "add_repos_001"
|
|
20
|
+
down_revision: str | None = "357c780ee445"
|
|
21
|
+
branch_labels: str | Sequence[str] | None = None
|
|
22
|
+
depends_on: str | Sequence[str] | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def upgrade() -> None:
|
|
26
|
+
# Create repos table
|
|
27
|
+
op.create_table(
|
|
28
|
+
"repos",
|
|
29
|
+
sa.Column("id", sa.String(length=36), nullable=False),
|
|
30
|
+
sa.Column("path", sa.String(length=1024), nullable=False),
|
|
31
|
+
sa.Column("name", sa.String(length=255), nullable=False),
|
|
32
|
+
sa.Column("display_name", sa.String(length=255), nullable=False),
|
|
33
|
+
sa.Column("setup_script", sa.Text(), nullable=True),
|
|
34
|
+
sa.Column("cleanup_script", sa.Text(), nullable=True),
|
|
35
|
+
sa.Column("dev_server_script", sa.Text(), nullable=True),
|
|
36
|
+
sa.Column("default_branch", sa.String(length=255), nullable=True),
|
|
37
|
+
sa.Column("remote_url", sa.String(length=1024), nullable=True),
|
|
38
|
+
sa.Column(
|
|
39
|
+
"created_at",
|
|
40
|
+
sa.DateTime(),
|
|
41
|
+
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
|
42
|
+
nullable=False,
|
|
43
|
+
),
|
|
44
|
+
sa.Column(
|
|
45
|
+
"updated_at",
|
|
46
|
+
sa.DateTime(),
|
|
47
|
+
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
|
48
|
+
nullable=False,
|
|
49
|
+
),
|
|
50
|
+
sa.PrimaryKeyConstraint("id"),
|
|
51
|
+
sa.UniqueConstraint("path", name="uq_repo_path"),
|
|
52
|
+
)
|
|
53
|
+
op.create_index(op.f("ix_repos_path"), "repos", ["path"], unique=True)
|
|
54
|
+
|
|
55
|
+
# Create board_repos junction table
|
|
56
|
+
op.create_table(
|
|
57
|
+
"board_repos",
|
|
58
|
+
sa.Column("id", sa.String(length=36), nullable=False),
|
|
59
|
+
sa.Column("board_id", sa.String(length=36), nullable=False),
|
|
60
|
+
sa.Column("repo_id", sa.String(length=36), nullable=False),
|
|
61
|
+
sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="0"),
|
|
62
|
+
sa.Column("custom_setup_script", sa.Text(), nullable=True),
|
|
63
|
+
sa.Column("custom_cleanup_script", sa.Text(), nullable=True),
|
|
64
|
+
sa.Column(
|
|
65
|
+
"created_at",
|
|
66
|
+
sa.DateTime(),
|
|
67
|
+
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
|
68
|
+
nullable=False,
|
|
69
|
+
),
|
|
70
|
+
sa.ForeignKeyConstraint(["board_id"], ["boards.id"], ondelete="CASCADE"),
|
|
71
|
+
sa.ForeignKeyConstraint(["repo_id"], ["repos.id"], ondelete="CASCADE"),
|
|
72
|
+
sa.PrimaryKeyConstraint("id"),
|
|
73
|
+
sa.UniqueConstraint("board_id", "repo_id", name="uq_board_repo"),
|
|
74
|
+
)
|
|
75
|
+
op.create_index(
|
|
76
|
+
op.f("ix_board_repos_board_id"), "board_repos", ["board_id"], unique=False
|
|
77
|
+
)
|
|
78
|
+
op.create_index(
|
|
79
|
+
op.f("ix_board_repos_repo_id"), "board_repos", ["repo_id"], unique=False
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Migrate existing boards.repo_root to new structure
|
|
83
|
+
# Note: This is a data migration, so we need to use SQLAlchemy core
|
|
84
|
+
|
|
85
|
+
# Define table structures for data migration
|
|
86
|
+
boards = table(
|
|
87
|
+
"boards",
|
|
88
|
+
column("id", sa.String),
|
|
89
|
+
column("repo_root", sa.String),
|
|
90
|
+
column("default_branch", sa.String),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
repos_table = table(
|
|
94
|
+
"repos",
|
|
95
|
+
column("id", sa.String),
|
|
96
|
+
column("path", sa.String),
|
|
97
|
+
column("name", sa.String),
|
|
98
|
+
column("display_name", sa.String),
|
|
99
|
+
column("default_branch", sa.String),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
board_repos_table = table(
|
|
103
|
+
"board_repos",
|
|
104
|
+
column("id", sa.String),
|
|
105
|
+
column("board_id", sa.String),
|
|
106
|
+
column("repo_id", sa.String),
|
|
107
|
+
column("is_primary", sa.Boolean),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Get connection
|
|
111
|
+
conn = op.get_bind()
|
|
112
|
+
|
|
113
|
+
# Fetch all boards with repo_root
|
|
114
|
+
result = conn.execute(
|
|
115
|
+
sa.select(boards.c.id, boards.c.repo_root, boards.c.default_branch)
|
|
116
|
+
)
|
|
117
|
+
board_data = result.fetchall()
|
|
118
|
+
|
|
119
|
+
# For each board, create a repo and link it
|
|
120
|
+
for board_id, repo_root, default_branch in board_data:
|
|
121
|
+
if not repo_root:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Generate repo ID
|
|
125
|
+
repo_id = str(uuid.uuid4())
|
|
126
|
+
|
|
127
|
+
# Extract repo name from path
|
|
128
|
+
try:
|
|
129
|
+
repo_name = Path(repo_root).name
|
|
130
|
+
except Exception:
|
|
131
|
+
repo_name = "repository"
|
|
132
|
+
|
|
133
|
+
# Insert repo
|
|
134
|
+
conn.execute(
|
|
135
|
+
repos_table.insert().values(
|
|
136
|
+
id=repo_id,
|
|
137
|
+
path=repo_root,
|
|
138
|
+
name=repo_name,
|
|
139
|
+
display_name=repo_name,
|
|
140
|
+
default_branch=default_branch,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Link board to repo (as primary)
|
|
145
|
+
board_repo_id = str(uuid.uuid4())
|
|
146
|
+
conn.execute(
|
|
147
|
+
board_repos_table.insert().values(
|
|
148
|
+
id=board_repo_id, board_id=board_id, repo_id=repo_id, is_primary=True
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Make repo_root nullable (backwards compatibility - keep for now)
|
|
153
|
+
# In a future migration, we can remove it entirely
|
|
154
|
+
with op.batch_alter_table("boards", schema=None) as batch_op:
|
|
155
|
+
batch_op.alter_column(
|
|
156
|
+
"repo_root", existing_type=sa.String(length=1024), nullable=True
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def downgrade() -> None:
|
|
161
|
+
# Drop board_repos table
|
|
162
|
+
op.drop_index(op.f("ix_board_repos_repo_id"), table_name="board_repos")
|
|
163
|
+
op.drop_index(op.f("ix_board_repos_board_id"), table_name="board_repos")
|
|
164
|
+
op.drop_table("board_repos")
|
|
165
|
+
|
|
166
|
+
# Drop repos table
|
|
167
|
+
op.drop_index(op.f("ix_repos_path"), table_name="repos")
|
|
168
|
+
op.drop_table("repos")
|
|
169
|
+
|
|
170
|
+
# Restore repo_root to non-nullable
|
|
171
|
+
with op.batch_alter_table("boards", schema=None) as batch_op:
|
|
172
|
+
batch_op.alter_column(
|
|
173
|
+
"repo_root", existing_type=sa.String(length=1024), nullable=False
|
|
174
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Add session_id column to jobs table for executor session resume.
|
|
2
|
+
|
|
3
|
+
Revision ID: a1b2c3d4e5f6
|
|
4
|
+
Revises: 7b307e847cbd
|
|
5
|
+
Create Date: 2026-02-16 15:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
from alembic import op
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "a1b2c3d4e5f6"
|
|
17
|
+
down_revision: str | None = "7b307e847cbd"
|
|
18
|
+
branch_labels: str | Sequence[str] | None = None
|
|
19
|
+
depends_on: str | Sequence[str] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
op.add_column("jobs", sa.Column("session_id", sa.String(255), nullable=True))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
op.drop_column("jobs", "session_id")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""add_sqlite_backend_tables
|
|
2
|
+
|
|
3
|
+
Revision ID: c3a8f1b2d4e5
|
|
4
|
+
Revises: b10fb0b62240
|
|
5
|
+
Create Date: 2026-02-15 10:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
from alembic import op
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "c3a8f1b2d4e5"
|
|
17
|
+
down_revision: str | None = "b10fb0b62240"
|
|
18
|
+
branch_labels: str | Sequence[str] | None = None
|
|
19
|
+
depends_on: str | Sequence[str] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
# Job queue (replaces Celery broker)
|
|
24
|
+
op.create_table(
|
|
25
|
+
"job_queue",
|
|
26
|
+
sa.Column("id", sa.String(36), primary_key=True),
|
|
27
|
+
sa.Column("task_name", sa.String(255), nullable=False),
|
|
28
|
+
sa.Column("args_json", sa.Text(), nullable=False, server_default="[]"),
|
|
29
|
+
sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
|
|
30
|
+
sa.Column("claimed_by", sa.String(255), nullable=True),
|
|
31
|
+
sa.Column("claimed_at", sa.DateTime(), nullable=True),
|
|
32
|
+
sa.Column("completed_at", sa.DateTime(), nullable=True),
|
|
33
|
+
sa.Column("result_json", sa.Text(), nullable=True),
|
|
34
|
+
sa.Column("priority", sa.Integer(), nullable=False, server_default="0"),
|
|
35
|
+
sa.Column(
|
|
36
|
+
"created_at",
|
|
37
|
+
sa.DateTime(),
|
|
38
|
+
nullable=False,
|
|
39
|
+
server_default=sa.func.now(),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
op.create_index("ix_job_queue_status", "job_queue", ["status"])
|
|
43
|
+
op.create_index(
|
|
44
|
+
"ix_job_queue_claim_order",
|
|
45
|
+
"job_queue",
|
|
46
|
+
["status", "priority", "created_at"],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Idempotency cache (replaces Redis SETNX + result cache)
|
|
50
|
+
op.create_table(
|
|
51
|
+
"idempotency_cache",
|
|
52
|
+
sa.Column("cache_key", sa.String(512), primary_key=True),
|
|
53
|
+
sa.Column("lock_value", sa.Text(), nullable=True),
|
|
54
|
+
sa.Column("result_value", sa.Text(), nullable=True),
|
|
55
|
+
sa.Column("lock_expires_at", sa.DateTime(), nullable=True),
|
|
56
|
+
sa.Column("result_expires_at", sa.DateTime(), nullable=True),
|
|
57
|
+
sa.Column(
|
|
58
|
+
"created_at",
|
|
59
|
+
sa.DateTime(),
|
|
60
|
+
nullable=False,
|
|
61
|
+
server_default=sa.func.now(),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Rate limit entries (replaces Redis sorted sets)
|
|
66
|
+
op.create_table(
|
|
67
|
+
"rate_limit_entries",
|
|
68
|
+
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
|
69
|
+
sa.Column("client_key", sa.String(255), nullable=False),
|
|
70
|
+
sa.Column("cost", sa.Integer(), nullable=False, server_default="1"),
|
|
71
|
+
sa.Column("recorded_at", sa.Float(), nullable=False),
|
|
72
|
+
sa.Column("expires_at", sa.Float(), nullable=False),
|
|
73
|
+
)
|
|
74
|
+
op.create_index(
|
|
75
|
+
"ix_rate_limit_entries_client_key",
|
|
76
|
+
"rate_limit_entries",
|
|
77
|
+
["client_key"],
|
|
78
|
+
)
|
|
79
|
+
op.create_index(
|
|
80
|
+
"ix_rate_limit_entries_expires_at",
|
|
81
|
+
"rate_limit_entries",
|
|
82
|
+
["expires_at"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# KV store (replaces Redis GET/SET for queued messages)
|
|
86
|
+
op.create_table(
|
|
87
|
+
"kv_store",
|
|
88
|
+
sa.Column("key", sa.String(512), primary_key=True),
|
|
89
|
+
sa.Column("value", sa.Text(), nullable=False),
|
|
90
|
+
sa.Column("expires_at", sa.DateTime(), nullable=True),
|
|
91
|
+
sa.Column(
|
|
92
|
+
"created_at",
|
|
93
|
+
sa.DateTime(),
|
|
94
|
+
nullable=False,
|
|
95
|
+
server_default=sa.func.now(),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def downgrade() -> None:
|
|
101
|
+
op.drop_table("kv_store")
|
|
102
|
+
op.drop_table("rate_limit_entries")
|
|
103
|
+
op.drop_table("idempotency_cache")
|
|
104
|
+
op.drop_table("job_queue")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""add_diff_content_to_revisions
|
|
2
|
+
|
|
3
|
+
Revision ID: b10fb0b62240
|
|
4
|
+
Revises: 9d17f0698d3b
|
|
5
|
+
Create Date: 2026-02-10 07:08:55.015460
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
from alembic import op
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "b10fb0b62240"
|
|
17
|
+
down_revision: str | None = "9d17f0698d3b"
|
|
18
|
+
branch_labels: str | Sequence[str] | None = None
|
|
19
|
+
depends_on: str | Sequence[str] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
# Add diff content columns to store diffs permanently in DB
|
|
24
|
+
# This preserves diffs even after worktree/evidence cleanup
|
|
25
|
+
op.add_column("revisions", sa.Column("diff_stat_content", sa.Text(), nullable=True))
|
|
26
|
+
op.add_column(
|
|
27
|
+
"revisions", sa.Column("diff_patch_content", sa.Text(), nullable=True)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def downgrade() -> None:
|
|
32
|
+
# Remove diff content columns
|
|
33
|
+
op.drop_column("revisions", "diff_patch_content")
|
|
34
|
+
op.drop_column("revisions", "diff_stat_content")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# A generic, single database configuration.
|
|
2
|
+
|
|
3
|
+
[alembic]
|
|
4
|
+
# path to migration scripts
|
|
5
|
+
script_location = alembic
|
|
6
|
+
|
|
7
|
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
8
|
+
# file_template = %%(rev)s_%%(slug)s
|
|
9
|
+
|
|
10
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
11
|
+
# defaults to the current working directory.
|
|
12
|
+
prepend_sys_path = .
|
|
13
|
+
|
|
14
|
+
# version path separator; As mentioned above, this is around a ';' character
|
|
15
|
+
# by default, but may be changed with the '--sep' flag to 'alembic init'.
|
|
16
|
+
# version_path_separator = os
|
|
17
|
+
|
|
18
|
+
# set to 'true' to run the environment during
|
|
19
|
+
# the 'revision' command, regardless of autogenerate
|
|
20
|
+
# revision_environment = false
|
|
21
|
+
|
|
22
|
+
# set to 'true' to allow .pyc and .pyo files without
|
|
23
|
+
# a source .py file to be detected as revisions in the
|
|
24
|
+
# versions/ directory
|
|
25
|
+
# sourceless = false
|
|
26
|
+
|
|
27
|
+
# version location specification; This defaults
|
|
28
|
+
# to alembic/versions. When using multiple version
|
|
29
|
+
# directories, initial revisions must be specified with --version-path.
|
|
30
|
+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
|
31
|
+
|
|
32
|
+
# set to 'true' to search source files recursively
|
|
33
|
+
# in each "version_locations" directory
|
|
34
|
+
# recursive_version_locations = false
|
|
35
|
+
|
|
36
|
+
# the output encoding used when revision files
|
|
37
|
+
# are written from script.py.mako
|
|
38
|
+
# output_encoding = utf-8
|
|
39
|
+
|
|
40
|
+
sqlalchemy.url = sqlite:///./kanban.db
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
[post_write_hooks]
|
|
44
|
+
# post_write_hooks defines scripts or Python functions that are run
|
|
45
|
+
# on newly generated revision scripts. See the documentation for further
|
|
46
|
+
# detail and examples
|
|
47
|
+
|
|
48
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
49
|
+
# hooks = black
|
|
50
|
+
# black.type = console_scripts
|
|
51
|
+
# black.entrypoint = black
|
|
52
|
+
# black.options = -q
|
|
53
|
+
|
|
54
|
+
# Logging configuration
|
|
55
|
+
[loggers]
|
|
56
|
+
keys = root,sqlalchemy,alembic
|
|
57
|
+
|
|
58
|
+
[handlers]
|
|
59
|
+
keys = console
|
|
60
|
+
|
|
61
|
+
[formatters]
|
|
62
|
+
keys = generic
|
|
63
|
+
|
|
64
|
+
[logger_root]
|
|
65
|
+
level = WARN
|
|
66
|
+
handlers = console
|
|
67
|
+
qualname =
|
|
68
|
+
|
|
69
|
+
[logger_sqlalchemy]
|
|
70
|
+
level = WARN
|
|
71
|
+
handlers =
|
|
72
|
+
qualname = sqlalchemy.engine
|
|
73
|
+
|
|
74
|
+
[logger_alembic]
|
|
75
|
+
level = INFO
|
|
76
|
+
handlers =
|
|
77
|
+
qualname = alembic
|
|
78
|
+
|
|
79
|
+
[handler_console]
|
|
80
|
+
class = StreamHandler
|
|
81
|
+
args = (sys.stderr,)
|
|
82
|
+
level = NOTSET
|
|
83
|
+
formatter = generic
|
|
84
|
+
|
|
85
|
+
[formatter_generic]
|
|
86
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
87
|
+
datefmt = %H:%M:%S
|
|
88
|
+
|
|
89
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Central data directory for Draft runtime artifacts.
|
|
2
|
+
|
|
3
|
+
All worktrees, logs, and evidence files live under a single central
|
|
4
|
+
directory (~/.draft/ by default) instead of polluting target repos.
|
|
5
|
+
|
|
6
|
+
Override with the DRAFT_DATA_DIR environment variable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_DEFAULT_DATA_DIR = Path.home() / ".draft"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_data_dir() -> Path:
|
|
16
|
+
"""Return the central data directory, creating it if needed.
|
|
17
|
+
|
|
18
|
+
Resolution order:
|
|
19
|
+
1. DRAFT_DATA_DIR environment variable
|
|
20
|
+
2. ~/.draft/
|
|
21
|
+
"""
|
|
22
|
+
data_dir = Path(os.environ.get("DRAFT_DATA_DIR", str(_DEFAULT_DATA_DIR)))
|
|
23
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
return data_dir
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_worktree_dir(board_id: str, ticket_id: str) -> Path:
|
|
28
|
+
"""Return the worktree directory for a ticket.
|
|
29
|
+
|
|
30
|
+
Layout: {data_dir}/worktrees/{board_id}/{ticket_id}/
|
|
31
|
+
"""
|
|
32
|
+
return get_data_dir() / "worktrees" / board_id / ticket_id
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_worktrees_root() -> Path:
|
|
36
|
+
"""Return the root worktrees directory.
|
|
37
|
+
|
|
38
|
+
Layout: {data_dir}/worktrees/
|
|
39
|
+
"""
|
|
40
|
+
return get_data_dir() / "worktrees"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_log_path(job_id: str) -> Path:
|
|
44
|
+
"""Return the log file path for a job.
|
|
45
|
+
|
|
46
|
+
Layout: {data_dir}/logs/{job_id}.log
|
|
47
|
+
"""
|
|
48
|
+
logs_dir = get_logs_dir()
|
|
49
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
return logs_dir / f"{job_id}.log"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_logs_dir() -> Path:
|
|
54
|
+
"""Return the central logs directory.
|
|
55
|
+
|
|
56
|
+
Layout: {data_dir}/logs/
|
|
57
|
+
"""
|
|
58
|
+
d = get_data_dir() / "logs"
|
|
59
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
return d
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_evidence_dir(job_id: str) -> Path:
|
|
64
|
+
"""Return the evidence directory for a job.
|
|
65
|
+
|
|
66
|
+
Layout: {data_dir}/evidence/{job_id}/
|
|
67
|
+
"""
|
|
68
|
+
d = get_data_dir() / "evidence" / job_id
|
|
69
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
return d
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_jobs_dir(job_id: str) -> Path:
|
|
74
|
+
"""Return the job working directory.
|
|
75
|
+
|
|
76
|
+
Layout: {data_dir}/jobs/{job_id}/
|
|
77
|
+
"""
|
|
78
|
+
d = get_data_dir() / "jobs" / job_id
|
|
79
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
return d
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Legacy path constant for migration-period checks
|
|
84
|
+
LEGACY_DRAFT_DIR = ".draft"
|
|
85
|
+
LEGACY_WORKTREES_DIR = ".draft/worktrees"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Database connection and session management for Draft."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import event
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
9
|
+
|
|
10
|
+
from app.models.base import Base
|
|
11
|
+
|
|
12
|
+
# Get the backend directory (where this file lives is app/, so go up one level)
|
|
13
|
+
_BACKEND_DIR = Path(__file__).parent.parent.resolve()
|
|
14
|
+
|
|
15
|
+
# Database URL - defaults to SQLite file in the backend directory
|
|
16
|
+
# Use absolute path to ensure consistency across different working directories
|
|
17
|
+
DATABASE_URL = os.getenv(
|
|
18
|
+
"DATABASE_URL", f"sqlite+aiosqlite:///{_BACKEND_DIR}/kanban.db"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_is_sqlite = "sqlite" in DATABASE_URL
|
|
22
|
+
|
|
23
|
+
# Create async engine with SQLite-friendly settings
|
|
24
|
+
engine = create_async_engine(
|
|
25
|
+
DATABASE_URL,
|
|
26
|
+
echo=os.getenv("SQL_ECHO", "false").lower() == "true",
|
|
27
|
+
future=True,
|
|
28
|
+
connect_args={"timeout": 30} if _is_sqlite else {},
|
|
29
|
+
pool_pre_ping=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Enable WAL mode for SQLite to prevent readers from blocking writers
|
|
34
|
+
if _is_sqlite:
|
|
35
|
+
|
|
36
|
+
@event.listens_for(engine.sync_engine, "connect")
|
|
37
|
+
def _set_sqlite_pragma(dbapi_conn, connection_record):
|
|
38
|
+
cursor = dbapi_conn.cursor()
|
|
39
|
+
cursor.execute("PRAGMA journal_mode=WAL")
|
|
40
|
+
cursor.execute("PRAGMA busy_timeout=30000")
|
|
41
|
+
cursor.close()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Session factory
|
|
45
|
+
async_session_maker = async_sessionmaker(
|
|
46
|
+
engine,
|
|
47
|
+
class_=AsyncSession,
|
|
48
|
+
expire_on_commit=False,
|
|
49
|
+
autocommit=False,
|
|
50
|
+
autoflush=False,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def init_db() -> None:
|
|
55
|
+
"""Initialize the database by creating all tables."""
|
|
56
|
+
async with engine.begin() as conn:
|
|
57
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
61
|
+
"""Dependency for FastAPI to inject database sessions."""
|
|
62
|
+
async with async_session_maker() as session:
|
|
63
|
+
try:
|
|
64
|
+
yield session
|
|
65
|
+
await session.commit()
|
|
66
|
+
except Exception:
|
|
67
|
+
await session.rollback()
|
|
68
|
+
raise
|
|
69
|
+
finally:
|
|
70
|
+
await session.close()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Synchronous database connection for Celery workers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import create_engine, event
|
|
9
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
10
|
+
|
|
11
|
+
# Get the backend directory (where this file lives is app/, so go up one level)
|
|
12
|
+
_BACKEND_DIR = Path(__file__).parent.parent.resolve()
|
|
13
|
+
|
|
14
|
+
# Synchronous database URL - defaults to SQLite file in the backend directory
|
|
15
|
+
# Use absolute path to ensure worker finds the same database as the server
|
|
16
|
+
DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{_BACKEND_DIR}/kanban.db")
|
|
17
|
+
if DATABASE_URL.startswith("sqlite+aiosqlite"):
|
|
18
|
+
DATABASE_URL = DATABASE_URL.replace("sqlite+aiosqlite", "sqlite")
|
|
19
|
+
|
|
20
|
+
_is_sqlite = DATABASE_URL.startswith("sqlite")
|
|
21
|
+
|
|
22
|
+
# Create synchronous engine with SQLite-friendly settings
|
|
23
|
+
sync_engine = create_engine(
|
|
24
|
+
DATABASE_URL,
|
|
25
|
+
echo=os.getenv("SQL_ECHO", "false").lower() == "true",
|
|
26
|
+
future=True,
|
|
27
|
+
connect_args={"timeout": 30} if _is_sqlite else {},
|
|
28
|
+
pool_pre_ping=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Enable WAL mode for SQLite to prevent readers from blocking writers
|
|
33
|
+
if _is_sqlite:
|
|
34
|
+
|
|
35
|
+
@event.listens_for(sync_engine, "connect")
|
|
36
|
+
def _set_sqlite_pragma(dbapi_conn, connection_record):
|
|
37
|
+
cursor = dbapi_conn.cursor()
|
|
38
|
+
cursor.execute("PRAGMA journal_mode=WAL")
|
|
39
|
+
cursor.execute("PRAGMA busy_timeout=30000")
|
|
40
|
+
cursor.close()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Synchronous session factory
|
|
44
|
+
SyncSessionLocal = sessionmaker(
|
|
45
|
+
bind=sync_engine,
|
|
46
|
+
class_=Session,
|
|
47
|
+
expire_on_commit=False,
|
|
48
|
+
autocommit=False,
|
|
49
|
+
autoflush=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def get_sync_db() -> Generator[Session, None, None]:
|
|
55
|
+
"""Context manager for synchronous database sessions (used by Celery workers)."""
|
|
56
|
+
session = SyncSessionLocal()
|
|
57
|
+
try:
|
|
58
|
+
yield session
|
|
59
|
+
session.commit()
|
|
60
|
+
except Exception:
|
|
61
|
+
session.rollback()
|
|
62
|
+
raise
|
|
63
|
+
finally:
|
|
64
|
+
session.close()
|