remote-coder 0.4.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.
- app/__init__.py +3 -0
- app/admin/__init__.py +0 -0
- app/admin/advanced_settings.py +88 -0
- app/admin/database_browser.py +301 -0
- app/admin/router.py +528 -0
- app/admin/static/i18n.js +401 -0
- app/admin/static/icons/advanced.svg +8 -0
- app/admin/static/icons/database.svg +5 -0
- app/admin/static/icons/download.svg +3 -0
- app/admin/static/icons/home.svg +4 -0
- app/admin/static/icons/logs.svg +3 -0
- app/admin/static/icons/projects.svg +5 -0
- app/admin/static/summary.js +73 -0
- app/admin/templates/admin.html +511 -0
- app/admin/templates/advanced.html +635 -0
- app/admin/templates/database.html +880 -0
- app/admin/templates/logs.html +686 -0
- app/admin/templates/projects.html +878 -0
- app/ai/__init__.py +0 -0
- app/ai/base.py +129 -0
- app/ai/claude.py +20 -0
- app/ai/codex.py +34 -0
- app/ai/factory.py +27 -0
- app/ai/gemini.py +20 -0
- app/ai/model_catalog.py +47 -0
- app/ai/usage.py +134 -0
- app/cli.py +238 -0
- app/config.py +130 -0
- app/git/__init__.py +0 -0
- app/git/ai_commit.py +88 -0
- app/git/branch_naming.py +21 -0
- app/git/commit_message.py +279 -0
- app/git/service.py +669 -0
- app/jobs/__init__.py +0 -0
- app/jobs/manager.py +770 -0
- app/jobs/schemas.py +116 -0
- app/jobs/store.py +334 -0
- app/main.py +265 -0
- app/models.py +20 -0
- app/monitoring/__init__.py +10 -0
- app/monitoring/code.py +161 -0
- app/monitoring/events.py +33 -0
- app/monitoring/git.py +103 -0
- app/monitoring/log_buffer.py +245 -0
- app/monitoring/memory.py +19 -0
- app/monitoring/model.py +598 -0
- app/projects/__init__.py +19 -0
- app/projects/registry.py +384 -0
- app/security/__init__.py +0 -0
- app/security/auth.py +19 -0
- app/system_startup.py +34 -0
- app/telegram/__init__.py +0 -0
- app/telegram/bot_instances.py +67 -0
- app/telegram/commands/__init__.py +64 -0
- app/telegram/commands/base.py +222 -0
- app/telegram/commands/branch.py +366 -0
- app/telegram/commands/clear_stop.py +221 -0
- app/telegram/commands/fix.py +219 -0
- app/telegram/commands/model.py +93 -0
- app/telegram/commands/monitor.py +185 -0
- app/telegram/commands/registry.py +110 -0
- app/telegram/commands/status.py +243 -0
- app/telegram/commands/system.py +201 -0
- app/telegram/confirmations.py +36 -0
- app/telegram/conversation.py +789 -0
- app/telegram/i18n.py +742 -0
- app/telegram/model_preferences.py +53 -0
- app/telegram/notifier.py +387 -0
- app/telegram/parser.py +267 -0
- app/telegram/webhook.py +988 -0
- app/telegram/webhook_registration.py +172 -0
- app/tunnel.py +104 -0
- remote_coder-0.4.1.dist-info/METADATA +520 -0
- remote_coder-0.4.1.dist-info/RECORD +78 -0
- remote_coder-0.4.1.dist-info/WHEEL +5 -0
- remote_coder-0.4.1.dist-info/entry_points.txt +2 -0
- remote_coder-0.4.1.dist-info/licenses/LICENSE +201 -0
- remote_coder-0.4.1.dist-info/top_level.txt +1 -0
app/jobs/schemas.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
|
9
|
+
|
|
10
|
+
from app.models import ModelName
|
|
11
|
+
|
|
12
|
+
_SAFE_BRANCH_TOKEN = re.compile(r"^[A-Za-z0-9/._-]+$")
|
|
13
|
+
_SAFE_JOB_ID_TOKEN = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JobStatus(StrEnum):
|
|
17
|
+
QUEUED = "queued"
|
|
18
|
+
RUNNING = "running"
|
|
19
|
+
SUCCEEDED = "succeeded"
|
|
20
|
+
FAILED = "failed"
|
|
21
|
+
CANCELLED = "cancelled"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JobMode(StrEnum):
|
|
25
|
+
AGENT = "agent"
|
|
26
|
+
PLAN = "plan"
|
|
27
|
+
ASK = "ask"
|
|
28
|
+
AGENT_FIX = "agent_fix"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FixKind(StrEnum):
|
|
32
|
+
COMMIT = "commit"
|
|
33
|
+
SOURCE = "source"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JobRequest(BaseModel):
|
|
37
|
+
project: str
|
|
38
|
+
model: ModelName
|
|
39
|
+
model_id: str | None = None
|
|
40
|
+
instruction: str
|
|
41
|
+
mode: JobMode = JobMode.AGENT
|
|
42
|
+
job_id: str | None = None
|
|
43
|
+
branch: str | None = None
|
|
44
|
+
commit: bool = True
|
|
45
|
+
chat_id: int
|
|
46
|
+
requested_by: int | None = None
|
|
47
|
+
message_id: int | None = None
|
|
48
|
+
reply_to_message_id: int | None = None
|
|
49
|
+
parent_job_id: str | None = None
|
|
50
|
+
fix_kind: FixKind | None = None
|
|
51
|
+
|
|
52
|
+
@field_validator("branch")
|
|
53
|
+
@classmethod
|
|
54
|
+
def _validate_branch(cls, value: str | None) -> str | None:
|
|
55
|
+
if value is None:
|
|
56
|
+
return None
|
|
57
|
+
if not value or len(value) > 255:
|
|
58
|
+
raise ValueError("branch is empty or too long")
|
|
59
|
+
if ".." in value or value.startswith("-") or not _SAFE_BRANCH_TOKEN.match(value):
|
|
60
|
+
raise ValueError("branch must use only ASCII letters, numbers, /, ., _, -")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
@field_validator("job_id", "parent_job_id")
|
|
64
|
+
@classmethod
|
|
65
|
+
def _validate_job_id(cls, value: str | None) -> str | None:
|
|
66
|
+
if value is None:
|
|
67
|
+
return None
|
|
68
|
+
if not value or len(value) > 128 or not _SAFE_JOB_ID_TOKEN.match(value):
|
|
69
|
+
raise ValueError("job_id must use only ASCII letters, numbers, ., _, -")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Job(BaseModel):
|
|
74
|
+
id: str
|
|
75
|
+
request: JobRequest
|
|
76
|
+
status: JobStatus = JobStatus.QUEUED
|
|
77
|
+
branch: str | None = None
|
|
78
|
+
commit_hash: str | None = None
|
|
79
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
80
|
+
error: str | None = None
|
|
81
|
+
error_stage: str | None = None
|
|
82
|
+
runner_actual_model: str | None = None
|
|
83
|
+
runner_token_usage: dict[str, int] = Field(default_factory=dict)
|
|
84
|
+
runner_stdout_summary: str | None = None
|
|
85
|
+
runner_stderr_summary: str | None = None
|
|
86
|
+
accepted_message_id: int | None = None
|
|
87
|
+
result_message_ids: list[int] = Field(default_factory=list)
|
|
88
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
89
|
+
started_at: datetime | None = None
|
|
90
|
+
finished_at: datetime | None = None
|
|
91
|
+
log_path: Path | None = None
|
|
92
|
+
|
|
93
|
+
def mark_running(self) -> None:
|
|
94
|
+
if self.status != JobStatus.QUEUED:
|
|
95
|
+
raise ValueError(f"Cannot move {self.status} to running")
|
|
96
|
+
self.status = JobStatus.RUNNING
|
|
97
|
+
self.started_at = datetime.now(UTC)
|
|
98
|
+
|
|
99
|
+
def mark_succeeded(self) -> None:
|
|
100
|
+
if self.status != JobStatus.RUNNING:
|
|
101
|
+
raise ValueError(f"Cannot move {self.status} to succeeded")
|
|
102
|
+
self.status = JobStatus.SUCCEEDED
|
|
103
|
+
self.finished_at = datetime.now(UTC)
|
|
104
|
+
|
|
105
|
+
def mark_failed(self, error: str) -> None:
|
|
106
|
+
if self.status not in (JobStatus.QUEUED, JobStatus.RUNNING):
|
|
107
|
+
raise ValueError(f"Cannot move {self.status} to failed")
|
|
108
|
+
self.status = JobStatus.FAILED
|
|
109
|
+
self.error = error
|
|
110
|
+
self.finished_at = datetime.now(UTC)
|
|
111
|
+
|
|
112
|
+
def mark_cancelled(self) -> None:
|
|
113
|
+
if self.status not in (JobStatus.QUEUED, JobStatus.RUNNING):
|
|
114
|
+
raise ValueError(f"Cannot move {self.status} to cancelled")
|
|
115
|
+
self.status = JobStatus.CANCELLED
|
|
116
|
+
self.finished_at = datetime.now(UTC)
|
app/jobs/store.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from app.jobs.schemas import Job, JobStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JobStore(Protocol):
|
|
12
|
+
def create(self, job: Job) -> None:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
def get(self, job_id: str) -> Job | None:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
def update(self, job: Job) -> None:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def list_recent(self, limit: int = 20) -> list[Job]:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def get_latest_succeeded_branch_for_project_chat(
|
|
25
|
+
self, project: str, chat_id: int
|
|
26
|
+
) -> str | None:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def list_recent_for_chat(self, chat_id: int, limit: int = 20) -> list[Job]:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def list_recent_for_project_chat(self, project: str, chat_id: int, limit: int = 20) -> list[Job]:
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InMemoryJobStore:
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self._jobs: dict[str, list[Job]] = {}
|
|
39
|
+
self._lock = Lock()
|
|
40
|
+
|
|
41
|
+
def create(self, job: Job) -> None:
|
|
42
|
+
with self._lock:
|
|
43
|
+
self._jobs.setdefault(job.id, []).append(job)
|
|
44
|
+
|
|
45
|
+
def get(self, job_id: str) -> Job | None:
|
|
46
|
+
with self._lock:
|
|
47
|
+
jobs = self._jobs.get(job_id)
|
|
48
|
+
return jobs[-1] if jobs else None
|
|
49
|
+
|
|
50
|
+
def update(self, job: Job) -> None:
|
|
51
|
+
with self._lock:
|
|
52
|
+
jobs = self._jobs.get(job.id)
|
|
53
|
+
if not jobs:
|
|
54
|
+
self._jobs[job.id] = [job]
|
|
55
|
+
return
|
|
56
|
+
for idx, existing in enumerate(jobs):
|
|
57
|
+
if existing.created_at == job.created_at:
|
|
58
|
+
jobs[idx] = job
|
|
59
|
+
return
|
|
60
|
+
jobs[-1] = job
|
|
61
|
+
|
|
62
|
+
def _all_jobs(self) -> list[Job]:
|
|
63
|
+
return [job for jobs in self._jobs.values() for job in jobs]
|
|
64
|
+
|
|
65
|
+
def list_recent(self, limit: int = 20) -> list[Job]:
|
|
66
|
+
with self._lock:
|
|
67
|
+
values = sorted(self._all_jobs(), key=lambda job: job.created_at, reverse=True)
|
|
68
|
+
return values[:limit]
|
|
69
|
+
|
|
70
|
+
def list_recent_for_chat(self, chat_id: int, limit: int = 20) -> list[Job]:
|
|
71
|
+
with self._lock:
|
|
72
|
+
values = [
|
|
73
|
+
job
|
|
74
|
+
for job in self._all_jobs()
|
|
75
|
+
if job.request.chat_id == chat_id
|
|
76
|
+
]
|
|
77
|
+
values.sort(key=lambda job: job.created_at, reverse=True)
|
|
78
|
+
return values[:limit]
|
|
79
|
+
|
|
80
|
+
def list_recent_for_project_chat(self, project: str, chat_id: int, limit: int = 20) -> list[Job]:
|
|
81
|
+
with self._lock:
|
|
82
|
+
values = [
|
|
83
|
+
job
|
|
84
|
+
for job in self._all_jobs()
|
|
85
|
+
if job.request.project == project and job.request.chat_id == chat_id
|
|
86
|
+
]
|
|
87
|
+
values.sort(key=lambda job: job.created_at, reverse=True)
|
|
88
|
+
return values[:limit]
|
|
89
|
+
|
|
90
|
+
def get_latest_succeeded_branch_for_project_chat(
|
|
91
|
+
self, project: str, chat_id: int
|
|
92
|
+
) -> str | None:
|
|
93
|
+
with self._lock:
|
|
94
|
+
candidates = [
|
|
95
|
+
j
|
|
96
|
+
for j in self._all_jobs()
|
|
97
|
+
if j.request.project == project
|
|
98
|
+
and j.request.chat_id == chat_id
|
|
99
|
+
and j.status == JobStatus.SUCCEEDED
|
|
100
|
+
and j.branch
|
|
101
|
+
]
|
|
102
|
+
if not candidates:
|
|
103
|
+
return None
|
|
104
|
+
candidates.sort(
|
|
105
|
+
key=lambda j: (j.finished_at or j.created_at, j.created_at),
|
|
106
|
+
reverse=True,
|
|
107
|
+
)
|
|
108
|
+
return candidates[0].branch
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _job_to_payload(job: Job) -> str:
|
|
112
|
+
return job.model_dump_json()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _payload_to_job(payload: str) -> Job:
|
|
116
|
+
return Job.model_validate_json(payload)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _job_sort_timestamp(job: Job) -> str:
|
|
120
|
+
return job.created_at.isoformat()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _job_finish_sort_timestamp(job: Job) -> str | None:
|
|
124
|
+
return (job.finished_at or job.created_at).isoformat()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SQLiteJobStore:
|
|
128
|
+
def __init__(self, db_path: Path) -> None:
|
|
129
|
+
self._db_path = db_path.resolve()
|
|
130
|
+
self._lock = Lock()
|
|
131
|
+
self.ensure_schema()
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def db_path(self) -> Path:
|
|
135
|
+
return self._db_path
|
|
136
|
+
|
|
137
|
+
def ensure_schema(self) -> None:
|
|
138
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
with self._lock:
|
|
140
|
+
conn = sqlite3.connect(self._db_path)
|
|
141
|
+
try:
|
|
142
|
+
conn.execute(
|
|
143
|
+
"""
|
|
144
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
145
|
+
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
146
|
+
job_id TEXT NOT NULL,
|
|
147
|
+
created_at TEXT NOT NULL,
|
|
148
|
+
finished_at TEXT,
|
|
149
|
+
request_project TEXT NOT NULL,
|
|
150
|
+
request_chat_id INTEGER NOT NULL,
|
|
151
|
+
status TEXT NOT NULL,
|
|
152
|
+
branch TEXT,
|
|
153
|
+
commit_hash TEXT,
|
|
154
|
+
payload TEXT NOT NULL
|
|
155
|
+
)
|
|
156
|
+
"""
|
|
157
|
+
)
|
|
158
|
+
conn.execute(
|
|
159
|
+
"""
|
|
160
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_job_created
|
|
161
|
+
ON jobs (job_id, created_at)
|
|
162
|
+
"""
|
|
163
|
+
)
|
|
164
|
+
conn.execute(
|
|
165
|
+
"""
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_recent
|
|
167
|
+
ON jobs (created_at DESC, row_id DESC)
|
|
168
|
+
"""
|
|
169
|
+
)
|
|
170
|
+
conn.execute(
|
|
171
|
+
"""
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_project_chat_recent
|
|
173
|
+
ON jobs (request_project, request_chat_id, created_at DESC, row_id DESC)
|
|
174
|
+
"""
|
|
175
|
+
)
|
|
176
|
+
conn.commit()
|
|
177
|
+
finally:
|
|
178
|
+
conn.close()
|
|
179
|
+
|
|
180
|
+
def create(self, job: Job) -> None:
|
|
181
|
+
with self._lock:
|
|
182
|
+
conn = sqlite3.connect(self._db_path)
|
|
183
|
+
try:
|
|
184
|
+
self._insert_job(conn, job)
|
|
185
|
+
conn.commit()
|
|
186
|
+
finally:
|
|
187
|
+
conn.close()
|
|
188
|
+
|
|
189
|
+
def get(self, job_id: str) -> Job | None:
|
|
190
|
+
with self._lock:
|
|
191
|
+
conn = sqlite3.connect(self._db_path)
|
|
192
|
+
try:
|
|
193
|
+
row = conn.execute(
|
|
194
|
+
"""
|
|
195
|
+
SELECT payload
|
|
196
|
+
FROM jobs
|
|
197
|
+
WHERE job_id = ?
|
|
198
|
+
ORDER BY created_at DESC, row_id DESC
|
|
199
|
+
LIMIT 1
|
|
200
|
+
""",
|
|
201
|
+
(job_id,),
|
|
202
|
+
).fetchone()
|
|
203
|
+
finally:
|
|
204
|
+
conn.close()
|
|
205
|
+
return _payload_to_job(str(row[0])) if row else None
|
|
206
|
+
|
|
207
|
+
def update(self, job: Job) -> None:
|
|
208
|
+
with self._lock:
|
|
209
|
+
conn = sqlite3.connect(self._db_path)
|
|
210
|
+
try:
|
|
211
|
+
cur = conn.execute(
|
|
212
|
+
"""
|
|
213
|
+
UPDATE jobs
|
|
214
|
+
SET finished_at = ?,
|
|
215
|
+
request_project = ?,
|
|
216
|
+
request_chat_id = ?,
|
|
217
|
+
status = ?,
|
|
218
|
+
branch = ?,
|
|
219
|
+
commit_hash = ?,
|
|
220
|
+
payload = ?
|
|
221
|
+
WHERE job_id = ? AND created_at = ?
|
|
222
|
+
""",
|
|
223
|
+
self._row_values(job)[1:] + (job.id, _job_sort_timestamp(job)),
|
|
224
|
+
)
|
|
225
|
+
if cur.rowcount == 0:
|
|
226
|
+
self._insert_job(conn, job)
|
|
227
|
+
conn.commit()
|
|
228
|
+
finally:
|
|
229
|
+
conn.close()
|
|
230
|
+
|
|
231
|
+
def list_recent(self, limit: int = 20) -> list[Job]:
|
|
232
|
+
if limit <= 0:
|
|
233
|
+
return []
|
|
234
|
+
return self._fetch_jobs(
|
|
235
|
+
"""
|
|
236
|
+
SELECT payload
|
|
237
|
+
FROM jobs
|
|
238
|
+
ORDER BY created_at DESC, row_id DESC
|
|
239
|
+
LIMIT ?
|
|
240
|
+
""",
|
|
241
|
+
(limit,),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def list_recent_for_chat(self, chat_id: int, limit: int = 20) -> list[Job]:
|
|
245
|
+
if limit <= 0:
|
|
246
|
+
return []
|
|
247
|
+
return self._fetch_jobs(
|
|
248
|
+
"""
|
|
249
|
+
SELECT payload
|
|
250
|
+
FROM jobs
|
|
251
|
+
WHERE request_chat_id = ?
|
|
252
|
+
ORDER BY created_at DESC, row_id DESC
|
|
253
|
+
LIMIT ?
|
|
254
|
+
""",
|
|
255
|
+
(chat_id, limit),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def list_recent_for_project_chat(self, project: str, chat_id: int, limit: int = 20) -> list[Job]:
|
|
259
|
+
if limit <= 0:
|
|
260
|
+
return []
|
|
261
|
+
return self._fetch_jobs(
|
|
262
|
+
"""
|
|
263
|
+
SELECT payload
|
|
264
|
+
FROM jobs
|
|
265
|
+
WHERE request_project = ? AND request_chat_id = ?
|
|
266
|
+
ORDER BY created_at DESC, row_id DESC
|
|
267
|
+
LIMIT ?
|
|
268
|
+
""",
|
|
269
|
+
(project, chat_id, limit),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def get_latest_succeeded_branch_for_project_chat(
|
|
273
|
+
self, project: str, chat_id: int
|
|
274
|
+
) -> str | None:
|
|
275
|
+
with self._lock:
|
|
276
|
+
conn = sqlite3.connect(self._db_path)
|
|
277
|
+
try:
|
|
278
|
+
row = conn.execute(
|
|
279
|
+
"""
|
|
280
|
+
SELECT branch
|
|
281
|
+
FROM jobs
|
|
282
|
+
WHERE request_project = ?
|
|
283
|
+
AND request_chat_id = ?
|
|
284
|
+
AND status = ?
|
|
285
|
+
AND branch IS NOT NULL
|
|
286
|
+
ORDER BY COALESCE(finished_at, created_at) DESC, created_at DESC, row_id DESC
|
|
287
|
+
LIMIT 1
|
|
288
|
+
""",
|
|
289
|
+
(project, chat_id, JobStatus.SUCCEEDED.value),
|
|
290
|
+
).fetchone()
|
|
291
|
+
finally:
|
|
292
|
+
conn.close()
|
|
293
|
+
return str(row[0]) if row else None
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def _row_values(job: Job) -> tuple[str, str | None, str, int, str, str | None, str | None, str]:
|
|
297
|
+
return (
|
|
298
|
+
_job_sort_timestamp(job),
|
|
299
|
+
_job_finish_sort_timestamp(job),
|
|
300
|
+
job.request.project,
|
|
301
|
+
job.request.chat_id,
|
|
302
|
+
job.status.value,
|
|
303
|
+
job.branch,
|
|
304
|
+
job.commit_hash,
|
|
305
|
+
_job_to_payload(job),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _insert_job(self, conn: sqlite3.Connection, job: Job) -> None:
|
|
309
|
+
conn.execute(
|
|
310
|
+
"""
|
|
311
|
+
INSERT INTO jobs (
|
|
312
|
+
job_id,
|
|
313
|
+
created_at,
|
|
314
|
+
finished_at,
|
|
315
|
+
request_project,
|
|
316
|
+
request_chat_id,
|
|
317
|
+
status,
|
|
318
|
+
branch,
|
|
319
|
+
commit_hash,
|
|
320
|
+
payload
|
|
321
|
+
)
|
|
322
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
323
|
+
""",
|
|
324
|
+
(job.id,) + self._row_values(job),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _fetch_jobs(self, query: str, params: tuple[object, ...]) -> list[Job]:
|
|
328
|
+
with self._lock:
|
|
329
|
+
conn = sqlite3.connect(self._db_path)
|
|
330
|
+
try:
|
|
331
|
+
rows = conn.execute(query, params).fetchall()
|
|
332
|
+
finally:
|
|
333
|
+
conn.close()
|
|
334
|
+
return [_payload_to_job(str(row[0])) for row in rows]
|