scriptgini 0.1.2__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.
@@ -0,0 +1,277 @@
1
+ import logging
2
+ import subprocess
3
+ import sys
4
+
5
+ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
6
+ from sqlalchemy.orm import Session
7
+
8
+ from app.database import get_db
9
+ from app.agents.script_gini_agent import run_agent
10
+ from app.models.bulk_job import BulkJob, BulkJobItem, BulkJobItemStatus, BulkJobKind, BulkJobStatus
11
+ from app.models.generated_script import GeneratedScript, ScriptStatus
12
+ from app.models.project import Project
13
+ from app.models.script_run import ScriptRunStatus
14
+ from app.models.test_case import TestCase
15
+ from app.schemas.bulk_job import BulkGenerateRequest, BulkJobResponse, BulkRunRequest
16
+ from app.routers.scripts import (
17
+ _create_script_run,
18
+ _get_project_or_404,
19
+ _run_python_script,
20
+ )
21
+
22
+ router = APIRouter(prefix="/projects/{project_id}/scripts", tags=["Bulk Jobs"])
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _get_bulk_job_or_404(project_id: int, job_id: int, db: Session) -> BulkJob:
27
+ job = db.query(BulkJob).filter(BulkJob.id == job_id, BulkJob.project_id == project_id).first()
28
+ if not job:
29
+ raise HTTPException(status_code=404, detail="Bulk job not found")
30
+ return job
31
+
32
+
33
+ def _refresh_job_counts(job: BulkJob, db: Session) -> None:
34
+ items = db.query(BulkJobItem).filter(BulkJobItem.job_id == job.id).all()
35
+ job.total_items = len(items)
36
+ job.completed_items = sum(1 for i in items if i.status == BulkJobItemStatus.completed)
37
+ job.failed_items = sum(1 for i in items if i.status == BulkJobItemStatus.failed)
38
+ job.skipped_items = sum(1 for i in items if i.status == BulkJobItemStatus.skipped)
39
+
40
+
41
+ def _serialize_bulk_job(job: BulkJob, db: Session) -> BulkJobResponse:
42
+ items = (
43
+ db.query(BulkJobItem)
44
+ .filter(BulkJobItem.job_id == job.id)
45
+ .order_by(BulkJobItem.created_at.asc(), BulkJobItem.id.asc())
46
+ .all()
47
+ )
48
+ payload = BulkJobResponse.model_validate(job)
49
+ payload.items = items
50
+ return payload
51
+
52
+
53
+ def _resolve_test_cases(project_id: int, ids: list[int] | None, db: Session) -> list[TestCase]:
54
+ query = db.query(TestCase).filter(TestCase.project_id == project_id)
55
+ if ids:
56
+ query = query.filter(TestCase.id.in_(ids))
57
+ return query.order_by(TestCase.id.asc()).all()
58
+
59
+
60
+ def _run_bulk_generation(job_id: int, request: BulkGenerateRequest):
61
+ from app.database import SessionLocal
62
+
63
+ db = SessionLocal()
64
+ try:
65
+ job = db.query(BulkJob).filter(BulkJob.id == job_id).first()
66
+ if not job:
67
+ return
68
+ job.status = BulkJobStatus.running
69
+ db.commit()
70
+
71
+ project = db.query(Project).filter(Project.id == job.project_id).first()
72
+ if not project:
73
+ job.status = BulkJobStatus.failed
74
+ db.commit()
75
+ return
76
+
77
+ items = db.query(BulkJobItem).filter(BulkJobItem.job_id == job.id).order_by(BulkJobItem.id.asc()).all()
78
+ framework = (request.framework or project.default_framework).value
79
+
80
+ for item in items:
81
+ tc = db.query(TestCase).filter(TestCase.id == item.test_case_id, TestCase.project_id == job.project_id).first()
82
+ if not tc:
83
+ item.status = BulkJobItemStatus.skipped
84
+ item.message = "Test case not found"
85
+ db.commit()
86
+ continue
87
+
88
+ item.status = BulkJobItemStatus.running
89
+ db.commit()
90
+
91
+ script_record = GeneratedScript(
92
+ project_id=job.project_id,
93
+ test_case_id=tc.id,
94
+ framework=framework,
95
+ llm_provider=request.llm_provider,
96
+ llm_model=request.llm_model or "",
97
+ status=ScriptStatus.generating,
98
+ )
99
+ db.add(script_record)
100
+ db.commit()
101
+ db.refresh(script_record)
102
+
103
+ try:
104
+ result = run_agent(
105
+ test_case_title=tc.title,
106
+ test_case_content=tc.content,
107
+ preconditions=tc.preconditions or "",
108
+ test_data_hints=tc.test_data_hints or "",
109
+ aut_base_url=project.aut_base_url,
110
+ framework=framework,
111
+ auth_hints=project.auth_hints or "",
112
+ llm_provider=request.llm_provider,
113
+ llm_model=request.llm_model,
114
+ )
115
+ script_record.script_content = result.get("script")
116
+ script_record.error_message = result.get("error")
117
+ script_record.token_usage = result.get("token_usage")
118
+ script_record.status = ScriptStatus.failed if result.get("error") else ScriptStatus.completed
119
+ item.status = BulkJobItemStatus.failed if result.get("error") else BulkJobItemStatus.completed
120
+ item.message = result.get("error")
121
+ except Exception as exc:
122
+ logger.exception("Bulk generate failed for test_case_id=%s", tc.id)
123
+ script_record.status = ScriptStatus.failed
124
+ script_record.error_message = str(exc)
125
+ item.status = BulkJobItemStatus.failed
126
+ item.message = str(exc)
127
+
128
+ item.script_id = script_record.id
129
+ db.commit()
130
+
131
+ _refresh_job_counts(job, db)
132
+ job.status = BulkJobStatus.failed if job.failed_items > 0 else BulkJobStatus.completed
133
+ db.commit()
134
+ finally:
135
+ db.close()
136
+
137
+
138
+ def _run_bulk_execution(job_id: int, request: BulkRunRequest):
139
+ from app.database import SessionLocal
140
+
141
+ db = SessionLocal()
142
+ try:
143
+ job = db.query(BulkJob).filter(BulkJob.id == job_id).first()
144
+ if not job:
145
+ return
146
+ job.status = BulkJobStatus.running
147
+ db.commit()
148
+
149
+ items = db.query(BulkJobItem).filter(BulkJobItem.job_id == job.id).order_by(BulkJobItem.id.asc()).all()
150
+ for item in items:
151
+ item.status = BulkJobItemStatus.running
152
+ db.commit()
153
+
154
+ latest_script = (
155
+ db.query(GeneratedScript)
156
+ .filter(
157
+ GeneratedScript.project_id == job.project_id,
158
+ GeneratedScript.test_case_id == item.test_case_id,
159
+ GeneratedScript.status == ScriptStatus.completed,
160
+ )
161
+ .order_by(GeneratedScript.created_at.desc(), GeneratedScript.id.desc())
162
+ .first()
163
+ )
164
+ if not latest_script:
165
+ item.status = BulkJobItemStatus.skipped
166
+ item.message = "No completed script available"
167
+ db.commit()
168
+ continue
169
+
170
+ item.script_id = latest_script.id
171
+ if latest_script.framework != "playwright_python":
172
+ item.status = BulkJobItemStatus.skipped
173
+ item.message = "Only playwright_python scripts can be executed"
174
+ db.commit()
175
+ continue
176
+
177
+ try:
178
+ run_result = _run_python_script(latest_script.script_content or "")
179
+ status = ScriptRunStatus.completed if run_result["success"] else ScriptRunStatus.failed
180
+ _create_script_run(db, latest_script, run_result, status)
181
+ item.status = BulkJobItemStatus.completed if run_result["success"] else BulkJobItemStatus.failed
182
+ item.message = None if run_result["success"] else "Scenario run failed"
183
+ except ValueError as exc:
184
+ run_result = {
185
+ "success": False,
186
+ "exit_code": 2,
187
+ "stdout": "",
188
+ "stderr": str(exc),
189
+ "duration_seconds": 0.0,
190
+ "command": f"{sys.executable} generated_scenario.py",
191
+ }
192
+ _create_script_run(db, latest_script, run_result, ScriptRunStatus.failed)
193
+ item.status = BulkJobItemStatus.failed
194
+ item.message = str(exc)
195
+ except subprocess.TimeoutExpired as exc:
196
+ run_result = {
197
+ "success": False,
198
+ "exit_code": 124,
199
+ "stdout": exc.stdout or "",
200
+ "stderr": (exc.stderr or "") + f"\nScript execution timed out after {exc.timeout} seconds",
201
+ "duration_seconds": float(exc.timeout),
202
+ "command": f"{sys.executable} generated_scenario.py",
203
+ }
204
+ _create_script_run(db, latest_script, run_result, ScriptRunStatus.timed_out)
205
+ item.status = BulkJobItemStatus.failed
206
+ item.message = f"Timed out after {exc.timeout} seconds"
207
+
208
+ db.commit()
209
+
210
+ _refresh_job_counts(job, db)
211
+ job.status = BulkJobStatus.failed if job.failed_items > 0 else BulkJobStatus.completed
212
+ db.commit()
213
+ finally:
214
+ db.close()
215
+
216
+
217
+ @router.post("/bulk-generate", response_model=BulkJobResponse, status_code=status.HTTP_202_ACCEPTED)
218
+ def bulk_generate_scripts(
219
+ project_id: int,
220
+ payload: BulkGenerateRequest,
221
+ background_tasks: BackgroundTasks,
222
+ db: Session = Depends(get_db),
223
+ ):
224
+ _get_project_or_404(project_id, db)
225
+ test_cases = _resolve_test_cases(project_id, payload.test_case_ids, db)
226
+ if not test_cases:
227
+ raise HTTPException(status_code=400, detail="No test cases found for bulk generation")
228
+
229
+ job = BulkJob(project_id=project_id, kind=BulkJobKind.generate, status=BulkJobStatus.pending)
230
+ db.add(job)
231
+ db.commit()
232
+ db.refresh(job)
233
+
234
+ for tc in test_cases:
235
+ db.add(BulkJobItem(job_id=job.id, test_case_id=tc.id, status=BulkJobItemStatus.pending))
236
+ db.commit()
237
+
238
+ _refresh_job_counts(job, db)
239
+ db.commit()
240
+
241
+ background_tasks.add_task(_run_bulk_generation, job.id, payload)
242
+ return _serialize_bulk_job(job, db)
243
+
244
+
245
+ @router.post("/bulk-run", response_model=BulkJobResponse, status_code=status.HTTP_202_ACCEPTED)
246
+ def bulk_run_scripts(
247
+ project_id: int,
248
+ payload: BulkRunRequest,
249
+ background_tasks: BackgroundTasks,
250
+ db: Session = Depends(get_db),
251
+ ):
252
+ _get_project_or_404(project_id, db)
253
+ test_cases = _resolve_test_cases(project_id, payload.test_case_ids, db)
254
+ if not test_cases:
255
+ raise HTTPException(status_code=400, detail="No test cases found for bulk run")
256
+
257
+ job = BulkJob(project_id=project_id, kind=BulkJobKind.run, status=BulkJobStatus.pending)
258
+ db.add(job)
259
+ db.commit()
260
+ db.refresh(job)
261
+
262
+ for tc in test_cases:
263
+ db.add(BulkJobItem(job_id=job.id, test_case_id=tc.id, status=BulkJobItemStatus.pending))
264
+ db.commit()
265
+
266
+ _refresh_job_counts(job, db)
267
+ db.commit()
268
+
269
+ background_tasks.add_task(_run_bulk_execution, job.id, payload)
270
+ return _serialize_bulk_job(job, db)
271
+
272
+
273
+ @router.get("/bulk-jobs/{job_id}", response_model=BulkJobResponse)
274
+ def get_bulk_job(project_id: int, job_id: int, db: Session = Depends(get_db)):
275
+ _get_project_or_404(project_id, db)
276
+ job = _get_bulk_job_or_404(project_id, job_id, db)
277
+ return _serialize_bulk_job(job, db)
app/routers/demo.py ADDED
@@ -0,0 +1,86 @@
1
+ from fastapi import APIRouter, Depends, status
2
+ from fastapi.responses import JSONResponse
3
+ from sqlalchemy.orm import Session
4
+
5
+ from app.database import get_db
6
+ from app.models.project import Project
7
+ from app.models.test_case import TestCase, TestCaseFormat
8
+
9
+ router = APIRouter(prefix="/demo", tags=["Demo"])
10
+
11
+ _DEMO_PROJECT_NAME = "Demo Retail Checkout"
12
+ _DEMO_AUT_BASE_URL = "https://demo.retail-checkout.local"
13
+
14
+ _DEMO_TEST_CASES = [
15
+ {
16
+ "title": "TC-001 Successful Login",
17
+ "format": TestCaseFormat.step_based,
18
+ "content": (
19
+ "Step 1: Navigate to /login\n"
20
+ "Step 2: Enter valid username and password\n"
21
+ "Step 3: Click Login\n"
22
+ "Expected: User is redirected to /dashboard"
23
+ ),
24
+ "preconditions": "Valid user account exists",
25
+ "test_data_hints": "username=demo.user, password=Demo@123",
26
+ },
27
+ {
28
+ "title": "TC-002 Add Product To Cart",
29
+ "format": TestCaseFormat.step_based,
30
+ "content": (
31
+ "Step 1: Navigate to /products\n"
32
+ "Step 2: Open a product detail page\n"
33
+ "Step 3: Click Add to Cart\n"
34
+ "Expected: Cart badge count increases by 1"
35
+ ),
36
+ "preconditions": "User is logged in",
37
+ "test_data_hints": "product=Basic Tee",
38
+ },
39
+ {
40
+ "title": "TC-003 Checkout Confirmation",
41
+ "format": TestCaseFormat.bdd,
42
+ "content": (
43
+ "Given a logged-in user with items in cart\n"
44
+ "When the user completes checkout with valid payment details\n"
45
+ "Then an order confirmation message is displayed"
46
+ ),
47
+ "preconditions": "At least one item is present in cart",
48
+ "test_data_hints": "payment_method=card",
49
+ },
50
+ ]
51
+
52
+
53
+ @router.post("/load", status_code=status.HTTP_201_CREATED)
54
+ def load_demo_project(db: Session = Depends(get_db)):
55
+ existing = db.query(Project).filter(Project.name == _DEMO_PROJECT_NAME).first()
56
+ if existing:
57
+ existing_count = db.query(TestCase).filter(TestCase.project_id == existing.id).count()
58
+ return JSONResponse(
59
+ status_code=status.HTTP_200_OK,
60
+ content={
61
+ "created": False,
62
+ "project_id": existing.id,
63
+ "project_name": existing.name,
64
+ "test_case_count": existing_count,
65
+ },
66
+ )
67
+
68
+ project = Project(
69
+ name=_DEMO_PROJECT_NAME,
70
+ aut_base_url=_DEMO_AUT_BASE_URL,
71
+ auth_hints="Use demo credentials on /login when needed.",
72
+ )
73
+ db.add(project)
74
+ db.commit()
75
+ db.refresh(project)
76
+
77
+ for item in _DEMO_TEST_CASES:
78
+ db.add(TestCase(project_id=project.id, **item))
79
+ db.commit()
80
+
81
+ return {
82
+ "created": True,
83
+ "project_id": project.id,
84
+ "project_name": project.name,
85
+ "test_case_count": len(_DEMO_TEST_CASES),
86
+ }
@@ -0,0 +1,51 @@
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+
4
+ from app.database import get_db
5
+ from app.models.project import Project
6
+ from app.schemas.project import ProjectCreate, ProjectUpdate, ProjectResponse
7
+
8
+ router = APIRouter(prefix="/projects", tags=["Projects"])
9
+
10
+
11
+ @router.post("/", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
12
+ def create_project(payload: ProjectCreate, db: Session = Depends(get_db)):
13
+ project = Project(**payload.model_dump())
14
+ db.add(project)
15
+ db.commit()
16
+ db.refresh(project)
17
+ return project
18
+
19
+
20
+ @router.get("/", response_model=list[ProjectResponse])
21
+ def list_projects(skip: int = 0, limit: int = 50, db: Session = Depends(get_db)):
22
+ return db.query(Project).offset(skip).limit(limit).all()
23
+
24
+
25
+ @router.get("/{project_id}", response_model=ProjectResponse)
26
+ def get_project(project_id: int, db: Session = Depends(get_db)):
27
+ project = db.query(Project).filter(Project.id == project_id).first()
28
+ if not project:
29
+ raise HTTPException(status_code=404, detail="Project not found")
30
+ return project
31
+
32
+
33
+ @router.patch("/{project_id}", response_model=ProjectResponse)
34
+ def update_project(project_id: int, payload: ProjectUpdate, db: Session = Depends(get_db)):
35
+ project = db.query(Project).filter(Project.id == project_id).first()
36
+ if not project:
37
+ raise HTTPException(status_code=404, detail="Project not found")
38
+ for field, value in payload.model_dump(exclude_none=True).items():
39
+ setattr(project, field, value)
40
+ db.commit()
41
+ db.refresh(project)
42
+ return project
43
+
44
+
45
+ @router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
46
+ def delete_project(project_id: int, db: Session = Depends(get_db)):
47
+ project = db.query(Project).filter(Project.id == project_id).first()
48
+ if not project:
49
+ raise HTTPException(status_code=404, detail="Project not found")
50
+ db.delete(project)
51
+ db.commit()