cpkit 0.1.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.
Files changed (90) hide show
  1. cpkit/README.md +82 -0
  2. cpkit/__init__.py +42 -0
  3. cpkit/admin.py +53 -0
  4. cpkit/app.py +130 -0
  5. cpkit/audit/README.md +33 -0
  6. cpkit/audit/__init__.py +44 -0
  7. cpkit/audit/events_service.py +36 -0
  8. cpkit/audit/recorder.py +240 -0
  9. cpkit/audit/repository.py +64 -0
  10. cpkit/audit/router.py +46 -0
  11. cpkit/audit/service.py +34 -0
  12. cpkit/audit/types.py +38 -0
  13. cpkit/auth/README.md +38 -0
  14. cpkit/auth/__init__.py +123 -0
  15. cpkit/auth/api_key_router.py +52 -0
  16. cpkit/auth/api_key_service.py +143 -0
  17. cpkit/auth/api_keys.py +143 -0
  18. cpkit/auth/bundle.py +113 -0
  19. cpkit/auth/claims.py +37 -0
  20. cpkit/auth/config.py +162 -0
  21. cpkit/auth/dependencies.py +155 -0
  22. cpkit/auth/oidc.py +703 -0
  23. cpkit/auth/redirects.py +12 -0
  24. cpkit/auth/repositories.py +181 -0
  25. cpkit/auth/router.py +214 -0
  26. cpkit/auth/secrets.py +78 -0
  27. cpkit/auth/types.py +49 -0
  28. cpkit/bundle.py +252 -0
  29. cpkit/cli/README.md +21 -0
  30. cpkit/cli/__init__.py +14 -0
  31. cpkit/cli/__main__.py +5 -0
  32. cpkit/cli/base.py +200 -0
  33. cpkit/cli/schema.py +30 -0
  34. cpkit/cli/server.py +22 -0
  35. cpkit/config/README.md +13 -0
  36. cpkit/config/__init__.py +9 -0
  37. cpkit/config/env.py +31 -0
  38. cpkit/db/README.md +24 -0
  39. cpkit/db/__init__.py +23 -0
  40. cpkit/db/postgres.py +252 -0
  41. cpkit/dependencies.py +73 -0
  42. cpkit/errors/README.md +22 -0
  43. cpkit/errors/__init__.py +35 -0
  44. cpkit/errors/http.py +45 -0
  45. cpkit/errors/repository.py +32 -0
  46. cpkit/errors/service.py +74 -0
  47. cpkit/jobs/README.md +29 -0
  48. cpkit/jobs/__init__.py +49 -0
  49. cpkit/jobs/maintenance.py +15 -0
  50. cpkit/jobs/repository.py +306 -0
  51. cpkit/jobs/router.py +105 -0
  52. cpkit/jobs/service.py +138 -0
  53. cpkit/jobs/types.py +67 -0
  54. cpkit/jobs/worker.py +200 -0
  55. cpkit/logging/README.md +19 -0
  56. cpkit/logging/__init__.py +13 -0
  57. cpkit/logging/context.py +29 -0
  58. cpkit/logging/middleware.py +55 -0
  59. cpkit/logging/setup.py +81 -0
  60. cpkit/playbooks/README.md +25 -0
  61. cpkit/playbooks/__init__.py +40 -0
  62. cpkit/playbooks/ansible.py +359 -0
  63. cpkit/playbooks/repository.py +95 -0
  64. cpkit/playbooks/router.py +95 -0
  65. cpkit/playbooks/service.py +232 -0
  66. cpkit/playbooks/types.py +43 -0
  67. cpkit/repository.py +63 -0
  68. cpkit/resources/__init__.py +17 -0
  69. cpkit/resources/ddl.sql +150 -0
  70. cpkit/settings/README.md +22 -0
  71. cpkit/settings/__init__.py +18 -0
  72. cpkit/settings/keys.py +30 -0
  73. cpkit/settings/repository.py +108 -0
  74. cpkit/settings/router.py +62 -0
  75. cpkit/settings/service.py +105 -0
  76. cpkit/settings/types.py +25 -0
  77. cpkit/time.py +4 -0
  78. cpkit/webapp/README.md +71 -0
  79. cpkit/webapp/__init__.py +12 -0
  80. cpkit/webapp/index.html +970 -0
  81. cpkit/webapp/script.js +1690 -0
  82. cpkit/webapp/style.css +1561 -0
  83. cpkit-0.1.0.dist-info/METADATA +105 -0
  84. cpkit-0.1.0.dist-info/RECORD +90 -0
  85. cpkit-0.1.0.dist-info/WHEEL +4 -0
  86. cpkit-0.1.0.dist-info/licenses/LICENSE +201 -0
  87. resources/README.md +16 -0
  88. resources/repository_maintenance_guide.md +90 -0
  89. resources/webapp_extension_guide.md +706 -0
  90. resources/webapp_extension_template.js +130 -0
@@ -0,0 +1,306 @@
1
+ """Repository helpers for the framework message queue."""
2
+
3
+ from typing import Any
4
+
5
+ from cpkit.db import execute_stmt, fetch_all, fetch_one
6
+
7
+ from .maintenance import FAIL_ZOMBIE_JOBS_MESSAGE_TYPE
8
+ from .types import IntID, Job, JobID, JobStatsResponse, LinkedResourceRef, Task
9
+
10
+ QUEUE_TABLE = "cpkit.mq"
11
+ JOBS_TABLE = "cpkit.jobs"
12
+ TASKS_TABLE = "cpkit.tasks"
13
+
14
+
15
+ class QueueRepositoryMixin:
16
+ """Repository mixin for enqueueing framework queue messages."""
17
+
18
+ def enqueue_message(
19
+ self,
20
+ msg_type: Any,
21
+ payload: Any | None,
22
+ created_by: str,
23
+ *,
24
+ start_after_seconds: int = 0,
25
+ ) -> None:
26
+ """Enqueue a message to be processed by the framework worker."""
27
+ execute_stmt(
28
+ f"""
29
+ INSERT INTO {QUEUE_TABLE}
30
+ (msg_type, msg_data, created_by, start_after)
31
+ VALUES
32
+ (%s, %s, %s, now() + (%s * INTERVAL '1s'))
33
+ """,
34
+ (
35
+ _message_type_value(msg_type),
36
+ _payload_value(payload),
37
+ created_by,
38
+ start_after_seconds,
39
+ ),
40
+ operation="jobs.enqueue_message",
41
+ )
42
+
43
+
44
+ class QueueJobRepositoryMixin(QueueRepositoryMixin):
45
+ """Repository mixin for enqueueing queue messages backed by job records."""
46
+
47
+ def enqueue_command(
48
+ self,
49
+ command_type: Any,
50
+ payload: Any,
51
+ created_by: str,
52
+ ) -> JobID:
53
+ payload_value = _payload_value(payload)
54
+ playbook_version = payload_value.get("playbook_version")
55
+ job_description = {
56
+ key: value for key, value in payload_value.items() if key != "playbook_version"
57
+ }
58
+ command_type_value = _message_type_value(command_type)
59
+ return fetch_one(
60
+ f"""
61
+ WITH
62
+ create_new_job AS (
63
+ INSERT INTO {QUEUE_TABLE}
64
+ (msg_type, msg_data, created_by)
65
+ VALUES
66
+ (%s, %s, %s)
67
+ RETURNING msg_id
68
+ )
69
+ INSERT INTO {JOBS_TABLE}
70
+ (job_id, job_type, status, playbook_version, description, created_by)
71
+ VALUES
72
+ ((select msg_id from create_new_job), %s, %s, %s, %s, %s)
73
+ RETURNING job_id AS job_id
74
+ """,
75
+ (
76
+ command_type_value,
77
+ payload_value,
78
+ created_by,
79
+ command_type_value,
80
+ "QUEUED",
81
+ str(playbook_version) if playbook_version is not None else None,
82
+ job_description,
83
+ created_by,
84
+ ),
85
+ JobID,
86
+ operation="jobs.enqueue_command",
87
+ )
88
+
89
+
90
+ class JobsRepositoryMixin:
91
+ """Repository mixin for framework job history, status, and task records."""
92
+
93
+ def get_job_stats(
94
+ self,
95
+ groups: list[str],
96
+ is_admin: bool = False,
97
+ ) -> JobStatsResponse:
98
+ if is_admin:
99
+ return (
100
+ fetch_one(
101
+ f"""
102
+ SELECT
103
+ COUNT(*) AS total,
104
+ COALESCE(SUM(CASE WHEN status = %s THEN 1 ELSE 0 END), 0) AS running,
105
+ COALESCE(SUM(CASE WHEN status = %s THEN 1 ELSE 0 END), 0) AS queued,
106
+ COALESCE(SUM(CASE WHEN status = %s THEN 1 ELSE 0 END), 0) AS failed
107
+ FROM {JOBS_TABLE}
108
+ """,
109
+ ("RUNNING", "QUEUED", "FAILED"),
110
+ JobStatsResponse,
111
+ )
112
+ or JobStatsResponse(total=0, running=0, queued=0, failed=0)
113
+ )
114
+
115
+ return (
116
+ fetch_one(
117
+ f"""
118
+ WITH
119
+ c AS (
120
+ SELECT cluster_id
121
+ FROM clusters
122
+ WHERE grp = ANY (%s)
123
+ ),
124
+ cj AS (
125
+ SELECT DISTINCT job_id
126
+ FROM {self.job_cluster_map_table}
127
+ WHERE cluster_id IN (SELECT * FROM c)
128
+ )
129
+ SELECT
130
+ COUNT(*) AS total,
131
+ COALESCE(SUM(CASE WHEN status = %s THEN 1 ELSE 0 END), 0) AS running,
132
+ COALESCE(SUM(CASE WHEN status = %s THEN 1 ELSE 0 END), 0) AS queued,
133
+ COALESCE(SUM(CASE WHEN status = %s THEN 1 ELSE 0 END), 0) AS failed
134
+ FROM {JOBS_TABLE}
135
+ WHERE job_id IN (SELECT * FROM cj)
136
+ """,
137
+ (groups, "RUNNING", "QUEUED", "FAILED"),
138
+ JobStatsResponse,
139
+ )
140
+ or JobStatsResponse(total=0, running=0, queued=0, failed=0)
141
+ )
142
+
143
+ def list_jobs(self, groups: list[str], is_admin: bool = False) -> list[Job]:
144
+ if is_admin:
145
+ return fetch_all(
146
+ f"""
147
+ SELECT *
148
+ FROM {JOBS_TABLE}
149
+ ORDER BY created_at DESC
150
+ """,
151
+ (),
152
+ Job,
153
+ )
154
+
155
+ return fetch_all(
156
+ f"""
157
+ WITH
158
+ c AS (
159
+ SELECT cluster_id
160
+ FROM clusters
161
+ WHERE grp = ANY (%s)
162
+ ),
163
+ cj AS (
164
+ SELECT job_id
165
+ FROM {self.job_cluster_map_table}
166
+ WHERE cluster_id IN (SELECT * FROM c)
167
+ )
168
+ SELECT *
169
+ FROM {JOBS_TABLE}
170
+ WHERE job_id IN (SELECT * FROM cj)
171
+ ORDER BY created_at DESC;
172
+ """,
173
+ (groups,),
174
+ Job,
175
+ )
176
+
177
+ def get_job(
178
+ self,
179
+ job_id: int,
180
+ groups: list[str],
181
+ is_admin: bool = False,
182
+ ) -> Job | None:
183
+ if is_admin:
184
+ return fetch_one(
185
+ f"""
186
+ SELECT *
187
+ FROM {JOBS_TABLE}
188
+ WHERE job_id = %s
189
+ """,
190
+ (job_id,),
191
+ Job,
192
+ )
193
+ return fetch_one(
194
+ f"""
195
+ WITH
196
+ c AS (
197
+ SELECT cluster_id
198
+ FROM clusters
199
+ WHERE grp = ANY (%s)
200
+ ),
201
+ cj AS (
202
+ SELECT job_id
203
+ FROM {self.job_cluster_map_table}
204
+ WHERE cluster_id IN (SELECT * FROM c)
205
+ )
206
+ SELECT *
207
+ FROM {JOBS_TABLE}
208
+ WHERE job_id IN (SELECT * FROM cj)
209
+ AND job_id = %s
210
+ """,
211
+ (groups, job_id),
212
+ Job,
213
+ )
214
+
215
+ def list_tasks(self, job_id: int) -> list[Task]:
216
+ return fetch_all(
217
+ f"""
218
+ SELECT job_id, task_id,
219
+ created_at, task_name, task_desc
220
+ FROM {TASKS_TABLE}
221
+ WHERE job_id = %s
222
+ ORDER BY task_id DESC
223
+ """,
224
+ (job_id,),
225
+ Task,
226
+ )
227
+
228
+ @property
229
+ def job_cluster_map_table(self) -> str:
230
+ raise NotImplementedError(
231
+ "Applications using group-scoped job visibility must provide "
232
+ "a job-cluster mapping table."
233
+ )
234
+
235
+ def list_linked_resources(self, job_id: int) -> list[LinkedResourceRef]:
236
+ return []
237
+
238
+ def update_job(self, job_id: int, status: str) -> None:
239
+ execute_stmt(
240
+ f"""
241
+ UPDATE {JOBS_TABLE}
242
+ SET status = %s
243
+ WHERE job_id = %s
244
+ """,
245
+ (_message_type_value(status), job_id),
246
+ )
247
+
248
+ def set_job_playbook_version(self, job_id: int, playbook_version: str) -> None:
249
+ execute_stmt(
250
+ f"""
251
+ UPDATE {JOBS_TABLE}
252
+ SET playbook_version = %s
253
+ WHERE job_id = %s
254
+ """,
255
+ (playbook_version, job_id),
256
+ )
257
+
258
+ def fail_zombie_jobs(self) -> list[IntID]:
259
+ return fetch_all(
260
+ f"""
261
+ WITH
262
+ fail_zombie_jobs AS (
263
+ INSERT INTO {QUEUE_TABLE} (msg_type, start_after)
264
+ VALUES (%s, now() + INTERVAL '300s' + (random()*10)::INTERVAL)
265
+ RETURNING 1
266
+ )
267
+ UPDATE {JOBS_TABLE}
268
+ SET status = %s
269
+ WHERE status in (%s, %s)
270
+ AND now() > updated_at + INTERVAL '300s'
271
+ RETURNING job_id AS id
272
+ """,
273
+ (FAIL_ZOMBIE_JOBS_MESSAGE_TYPE, "FAILED", "RUNNING", "QUEUED"),
274
+ IntID,
275
+ )
276
+
277
+ def create_task(
278
+ self,
279
+ job_id: int,
280
+ task_id: int,
281
+ created_at,
282
+ task_name: str,
283
+ task_desc,
284
+ ) -> None:
285
+ execute_stmt(
286
+ f"""
287
+ INSERT INTO {TASKS_TABLE}
288
+ (job_id, task_id, created_at, task_name, task_desc)
289
+ VALUES (%s, %s, %s, %s, %s)
290
+ """,
291
+ (job_id, task_id, created_at, task_name, task_desc),
292
+ )
293
+
294
+
295
+ def _message_type_value(msg_type: Any) -> str:
296
+ return str(getattr(msg_type, "value", msg_type))
297
+
298
+
299
+ def _payload_value(payload: Any | None) -> dict[str, Any]:
300
+ if payload is None:
301
+ return {}
302
+ if hasattr(payload, "model_dump"):
303
+ return payload.model_dump()
304
+ if isinstance(payload, dict):
305
+ return payload
306
+ raise TypeError("Queue message payload must be a mapping or Pydantic model.")
cpkit/jobs/router.py ADDED
@@ -0,0 +1,105 @@
1
+ """FastAPI routes for framework job management."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, status
7
+
8
+ from .types import Job, JobDetailsResponse, JobRescheduleResponse, JobStatsResponse
9
+
10
+
11
+ def create_jobs_router(
12
+ *,
13
+ get_service: Callable[..., Any],
14
+ get_access_scope: Callable[[dict], tuple[list[str], bool]],
15
+ get_audit_actor: Callable[..., Any],
16
+ require_readonly: Callable[..., Any],
17
+ require_user: Callable[..., Any],
18
+ handle_service_error: Callable[[Exception], None],
19
+ service_error_type: type[Exception] = Exception,
20
+ ) -> APIRouter:
21
+ router = APIRouter(prefix="/jobs", tags=["cpkit"])
22
+
23
+ @router.get("/")
24
+ async def list_jobs(
25
+ claims: dict = Depends(require_readonly),
26
+ service=Depends(get_service),
27
+ ) -> list[Job]:
28
+ groups, is_admin = get_access_scope(claims)
29
+ try:
30
+ return service.list_visible_jobs(groups, is_admin)
31
+ except service_error_type as err:
32
+ handle_service_error(err)
33
+
34
+ @router.get("/stats", response_model=JobStatsResponse)
35
+ async def get_job_stats(
36
+ claims: dict = Depends(require_readonly),
37
+ service=Depends(get_service),
38
+ ) -> JobStatsResponse:
39
+ groups, is_admin = get_access_scope(claims)
40
+ try:
41
+ return service.get_visible_job_stats(groups, is_admin)
42
+ except service_error_type as err:
43
+ handle_service_error(err)
44
+
45
+ @router.get("/{job_id}")
46
+ async def get_job(
47
+ job_id: int,
48
+ claims: dict = Depends(require_readonly),
49
+ service=Depends(get_service),
50
+ ) -> Job:
51
+ groups, is_admin = get_access_scope(claims)
52
+ try:
53
+ job = service.get_job_for_user(job_id, groups, is_admin)
54
+ except service_error_type as err:
55
+ handle_service_error(err)
56
+
57
+ if job is None:
58
+ raise HTTPException(
59
+ status_code=status.HTTP_404_NOT_FOUND,
60
+ detail=f"Job '{job_id}' was not found.",
61
+ )
62
+
63
+ return job
64
+
65
+ @router.get("/{job_id}/details", response_model=JobDetailsResponse)
66
+ async def get_job_details(
67
+ job_id: int,
68
+ claims: dict = Depends(require_readonly),
69
+ service=Depends(get_service),
70
+ ) -> JobDetailsResponse:
71
+ groups, is_admin = get_access_scope(claims)
72
+ try:
73
+ details = service.get_job_details_for_user(job_id, groups, is_admin)
74
+ except service_error_type as err:
75
+ handle_service_error(err)
76
+
77
+ if details is None:
78
+ raise HTTPException(
79
+ status_code=status.HTTP_404_NOT_FOUND,
80
+ detail=f"Job '{job_id}' was not found.",
81
+ )
82
+
83
+ return JobDetailsResponse(**details)
84
+
85
+ @router.post("/{job_id}/reschedule", response_model=JobRescheduleResponse)
86
+ async def reschedule_job(
87
+ job_id: int,
88
+ claims: dict = Depends(require_user),
89
+ actor_id: str = Depends(get_audit_actor),
90
+ service=Depends(get_service),
91
+ ) -> JobRescheduleResponse:
92
+ groups, is_admin = get_access_scope(claims)
93
+ try:
94
+ new_job_id = service.enqueue_job_reschedule(
95
+ job_id,
96
+ groups,
97
+ is_admin,
98
+ actor_id,
99
+ )
100
+ except service_error_type as err:
101
+ handle_service_error(err)
102
+
103
+ return JobRescheduleResponse(job_id=new_job_id)
104
+
105
+ return router
cpkit/jobs/service.py ADDED
@@ -0,0 +1,138 @@
1
+ """Service helpers for framework job history and rescheduling."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from cpkit.errors import RepositoryError, ServiceNotFoundError, from_repository_error
9
+
10
+ from .types import Job, JobID, JobStatsResponse
11
+
12
+ PayloadParser = Callable[[Any, dict[str, Any]], Any]
13
+ RescheduleTypeResolver = Callable[[str], Any]
14
+ JobAuditHook = Callable[[Any, str, str, dict[str, Any]], None]
15
+
16
+
17
+ class JobsService:
18
+ def __init__(
19
+ self,
20
+ repo,
21
+ *,
22
+ parse_payload: PayloadParser,
23
+ reschedule_type_resolver: RescheduleTypeResolver,
24
+ rescheduled_hook: JobAuditHook | None = None,
25
+ ) -> None:
26
+ self.repo = repo
27
+ self.parse_payload = parse_payload
28
+ self.reschedule_type_resolver = reschedule_type_resolver
29
+ self.rescheduled_hook = rescheduled_hook
30
+
31
+ def list_visible_jobs(self, groups: list[str], is_admin: bool) -> list[Job]:
32
+ try:
33
+ return self.repo.list_jobs(groups, is_admin)
34
+ except RepositoryError as err:
35
+ raise from_repository_error(
36
+ err,
37
+ unavailable_message="Jobs are temporarily unavailable.",
38
+ fallback_message="Unable to load jobs.",
39
+ ) from err
40
+
41
+ def get_visible_job_stats(
42
+ self,
43
+ groups: list[str],
44
+ is_admin: bool,
45
+ ) -> JobStatsResponse:
46
+ try:
47
+ return self.repo.get_job_stats(groups, is_admin)
48
+ except RepositoryError as err:
49
+ raise from_repository_error(
50
+ err,
51
+ unavailable_message="Job stats are temporarily unavailable.",
52
+ fallback_message="Unable to load job stats.",
53
+ ) from err
54
+
55
+ def get_job_for_user(
56
+ self,
57
+ job_id: int,
58
+ groups: list[str],
59
+ is_admin: bool,
60
+ ) -> Job | None:
61
+ try:
62
+ return self.repo.get_job(job_id, groups, is_admin)
63
+ except RepositoryError as err:
64
+ raise from_repository_error(
65
+ err,
66
+ unavailable_message="Job details are temporarily unavailable.",
67
+ fallback_message=f"Unable to load job '{job_id}'.",
68
+ ) from err
69
+
70
+ def get_job_details_for_user(
71
+ self,
72
+ job_id: int,
73
+ groups: list[str],
74
+ is_admin: bool,
75
+ ) -> dict[str, Any] | None:
76
+ selected_job = self.get_job_for_user(job_id, groups, is_admin)
77
+ if selected_job is None:
78
+ return None
79
+
80
+ try:
81
+ return {
82
+ "job": selected_job,
83
+ "description_yaml": yaml.dump(selected_job.description),
84
+ "tasks": self.repo.list_tasks(job_id),
85
+ "linked_resources": self.repo.list_linked_resources(job_id),
86
+ }
87
+ except RepositoryError as err:
88
+ raise from_repository_error(
89
+ err,
90
+ unavailable_message="Job details are temporarily unavailable.",
91
+ fallback_message=f"Unable to load tasks for job '{job_id}'.",
92
+ ) from err
93
+
94
+ def enqueue_job_reschedule(
95
+ self,
96
+ job_id: int,
97
+ groups: list[str],
98
+ is_admin: bool,
99
+ requested_by: str,
100
+ ) -> int:
101
+ selected_job = self.get_job_for_user(job_id, groups, is_admin)
102
+ if selected_job is None:
103
+ raise ServiceNotFoundError(f"Job '{job_id}' was not found.")
104
+
105
+ command_type = self.reschedule_type_resolver(selected_job.job_type)
106
+ payload = self.parse_payload(command_type, selected_job.description)
107
+
108
+ try:
109
+ msg_id: JobID = self.repo.enqueue_command(
110
+ command_type,
111
+ payload,
112
+ requested_by,
113
+ )
114
+ if self.rescheduled_hook is not None:
115
+ self.rescheduled_hook(
116
+ self.repo,
117
+ requested_by,
118
+ "JOB_RESCHEDULE_REQUESTED",
119
+ _payload_value(payload) | {"job_id": msg_id.job_id},
120
+ )
121
+ return msg_id.job_id
122
+ except RepositoryError as err:
123
+ raise from_repository_error(
124
+ err,
125
+ unavailable_message="Job rescheduling is temporarily unavailable.",
126
+ validation_message=(
127
+ "The job could not be rescheduled with its current payload."
128
+ ),
129
+ fallback_message=f"Unable to reschedule job '{job_id}'.",
130
+ ) from err
131
+
132
+
133
+ def _payload_value(payload: Any) -> dict[str, Any]:
134
+ if hasattr(payload, "model_dump"):
135
+ return payload.model_dump()
136
+ if isinstance(payload, dict):
137
+ return payload
138
+ return {"payload": payload}
cpkit/jobs/types.py ADDED
@@ -0,0 +1,67 @@
1
+ """Generic job queue data types."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class QueueMessage(BaseModel):
10
+ """Message claimed from the framework queue."""
11
+
12
+ msg_id: int
13
+ start_after: datetime
14
+ msg_type: str
15
+ msg_data: dict[str, Any] = Field(default_factory=dict)
16
+ created_at: datetime
17
+ created_by: str
18
+
19
+
20
+ class JobID(BaseModel):
21
+ job_id: int
22
+
23
+
24
+ class IntID(BaseModel):
25
+ id: int
26
+
27
+
28
+ class LinkedResourceRef(BaseModel):
29
+ resource_type: str
30
+ resource_id: str
31
+
32
+
33
+ class JobStatsResponse(BaseModel):
34
+ total: int
35
+ running: int
36
+ queued: int
37
+ failed: int
38
+
39
+
40
+ class Job(BaseModel):
41
+ job_id: int
42
+ job_type: str
43
+ status: str | None = None
44
+ playbook_version: str | None = None
45
+ description: dict[str, Any] = Field(default_factory=dict)
46
+ created_at: datetime
47
+ created_by: str | None = None
48
+ updated_at: datetime
49
+
50
+
51
+ class Task(BaseModel):
52
+ job_id: int
53
+ task_id: int
54
+ created_at: datetime
55
+ task_name: str | None = None
56
+ task_desc: str | None = None
57
+
58
+
59
+ class JobDetailsResponse(BaseModel):
60
+ job: Job
61
+ description_yaml: str
62
+ tasks: list[Task]
63
+ linked_resources: list[LinkedResourceRef]
64
+
65
+
66
+ class JobRescheduleResponse(BaseModel):
67
+ job_id: int