scriptgini 1.0.6__tar.gz → 1.2.0__tar.gz
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.
- {scriptgini-1.0.6 → scriptgini-1.2.0}/PKG-INFO +71 -2
- {scriptgini-1.0.6 → scriptgini-1.2.0}/README.md +70 -1
- scriptgini-1.2.0/app/__init__.py +3 -0
- scriptgini-1.2.0/app/cache.py +199 -0
- scriptgini-1.2.0/app/celery_app.py +30 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/config.py +18 -2
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/database.py +7 -2
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/main.py +22 -15
- scriptgini-1.2.0/app/models/api_key.py +35 -0
- scriptgini-1.2.0/app/models/membership.py +54 -0
- scriptgini-1.2.0/app/models/organization.py +24 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/project.py +3 -1
- scriptgini-1.2.0/app/models/user.py +32 -0
- scriptgini-1.2.0/app/routers/api_key.py +126 -0
- scriptgini-1.2.0/app/routers/auth.py +114 -0
- scriptgini-1.2.0/app/routers/organizations.py +36 -0
- scriptgini-1.2.0/app/routers/projects.py +113 -0
- scriptgini-1.2.0/app/schemas/api_key.py +76 -0
- scriptgini-1.2.0/app/schemas/auth.py +43 -0
- scriptgini-1.2.0/app/schemas/membership.py +22 -0
- scriptgini-1.2.0/app/schemas/organization.py +19 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/project.py +3 -0
- scriptgini-1.2.0/app/services/api_key.py +179 -0
- scriptgini-1.2.0/app/services/auth.py +101 -0
- scriptgini-1.2.0/app/services/auth_dependencies.py +110 -0
- scriptgini-1.2.0/app/services/rbac.py +118 -0
- scriptgini-1.2.0/app/tasks.py +138 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/pyproject.toml +8 -1
- {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/PKG-INFO +71 -2
- {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/SOURCES.txt +22 -1
- {scriptgini-1.0.6 → scriptgini-1.2.0}/tests/test_api.py +1 -0
- scriptgini-1.2.0/tests/test_auth.py +437 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/tests/test_coverage.py +1 -0
- scriptgini-1.2.0/tests/test_infra_services_coverage.py +439 -0
- scriptgini-1.2.0/tests/test_sprint2_rbac.py +295 -0
- scriptgini-1.0.6/app/routers/projects.py +0 -51
- scriptgini-1.0.6/app/schemas/__init__.py +0 -0
- {scriptgini-1.0.6/app → scriptgini-1.2.0/app/agents}/__init__.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/agents/prompts.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/agents/script_gini_agent.py +0 -0
- {scriptgini-1.0.6/app/agents → scriptgini-1.2.0/app/llm}/__init__.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/llm/provider.py +0 -0
- {scriptgini-1.0.6/app/llm → scriptgini-1.2.0/app/models}/__init__.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/bulk_job.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/generated_script.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/script_run.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/test_case.py +0 -0
- {scriptgini-1.0.6/app/models → scriptgini-1.2.0/app/routers}/__init__.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/analytics.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/bulk_jobs.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/demo.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/scripts.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/test_cases.py +0 -0
- {scriptgini-1.0.6/app/routers → scriptgini-1.2.0/app/schemas}/__init__.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/analytics.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/bulk_job.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/generated_script.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/test_case.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/app/services/git_export.py +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/dependency_links.txt +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/top_level.txt +0 -0
- {scriptgini-1.0.6 → scriptgini-1.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scriptgini
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Agentic AI system that converts functional test cases into automation test scripts.
|
|
5
5
|
Author: ScriptGini Team
|
|
6
6
|
License: Proprietary
|
|
@@ -127,7 +127,28 @@ If local generation feels slow, reduce `OLLAMA_NUM_PREDICT`, keep `SKIP_REVIEW_F
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
##
|
|
130
|
+
## OpenAPI Specification
|
|
131
|
+
|
|
132
|
+
**ScriptGini implements a production-ready OpenAPI 3.0.3 specification** that defines a complete enterprise REST API with 50+ endpoints, 60+ schemas, and comprehensive documentation.
|
|
133
|
+
|
|
134
|
+
### Specification Highlights
|
|
135
|
+
|
|
136
|
+
**The API specification includes 12 salient features**:
|
|
137
|
+
|
|
138
|
+
1. **Multi-tenant Architecture** — Organizations, Workspaces, Teams with hierarchical RBAC
|
|
139
|
+
2. **Advanced LLM Management** — Multi-provider orchestration, health monitoring, cost tracking, and model governance
|
|
140
|
+
3. **Intelligent Test Data Management** — Multi-source ingestion, synthetic generation, PII masking, state locking, placeholder mapping
|
|
141
|
+
4. **Asynchronous Job Management** — HTTP 202 Accepted, idempotent execution, job polling, webhooks
|
|
142
|
+
5. **Comprehensive Reporting** — Structured logs, artifact storage, signed downloads, execution diagnostics
|
|
143
|
+
6. **Analytics & Insights** — Dashboards, trend analysis, flakiness detection, code coverage, LLM usage
|
|
144
|
+
7. **Defect Lifecycle** — Auto-detection, Jira/Azure/GitHub sync, severity tracking, traceability
|
|
145
|
+
8. **Script Versioning** — Git-style history, quality metrics, refactoring, diff metadata
|
|
146
|
+
9. **Webhooks** — Event-driven notifications, delivery guarantees, retry policies
|
|
147
|
+
10. **Security & Authorization** — JWT + OAuth2, API keys with scopes, RBAC enforcement, rate limiting
|
|
148
|
+
11. **Error Handling & Observability** — Standardized error envelope, request ID tracing, correlation IDs
|
|
149
|
+
12. **Pagination & Filtering** — limit/offset/sort, comprehensive filtering, faceted search
|
|
150
|
+
|
|
151
|
+
### API Reference
|
|
131
152
|
|
|
132
153
|
Once running, visit:
|
|
133
154
|
|
|
@@ -136,6 +157,33 @@ Once running, visit:
|
|
|
136
157
|
| `http://localhost:8000/docs` | Swagger UI (interactive) |
|
|
137
158
|
| `http://localhost:8000/redoc` | ReDoc |
|
|
138
159
|
| `http://localhost:8000/health` | Health check |
|
|
160
|
+
| `http://localhost:8000/openapi.json` | Raw OpenAPI 3.0.3 specification |
|
|
161
|
+
|
|
162
|
+
### Full OpenAPI Documentation
|
|
163
|
+
|
|
164
|
+
- **Complete Specification**: [docs/openapi-improved-draft-2026-05-10.yaml](docs/openapi-improved-draft-2026-05-10.yaml)
|
|
165
|
+
- **Feature Documentation**: [docs/OPENAPI-SPECIFICATION.md](docs/OPENAPI-SPECIFICATION.md) — Covers all 12 salient features with examples and flows
|
|
166
|
+
- **API Architecture Review**: [docs/api-architecture-review-2026-05-10.md](docs/api-architecture-review-2026-05-10.md) — Analysis of current state vs. target enterprise API
|
|
167
|
+
|
|
168
|
+
### API Domains (50+ Endpoints)
|
|
169
|
+
|
|
170
|
+
| Domain | Endpoints | Purpose |
|
|
171
|
+
|--------|-----------|---------|
|
|
172
|
+
| **Authentication & IAM** | 8 | Login, token refresh, user management, API keys |
|
|
173
|
+
| **Organizations & Teams** | 6 | Multi-tenancy, team management, RBAC |
|
|
174
|
+
| **Workspaces & Projects** | 5 | Organizational hierarchy, configuration |
|
|
175
|
+
| **Test Data Management** | 11 | Data sets, reservations, masking, synthetic generation |
|
|
176
|
+
| **LLM Management** | 5 | Provider registration, model governance, cost tracking |
|
|
177
|
+
| **Script Engineering** | 7 | Generation, versioning, quality, refactoring |
|
|
178
|
+
| **Test Cases** | 4 | CRUD operations for test definitions |
|
|
179
|
+
| **Test Orchestration** | 5 | Execution, cancellation, job management |
|
|
180
|
+
| **Execution Reporting** | 4 | Reports, logs, artifacts, diagnostics |
|
|
181
|
+
| **Analytics & Insights** | 5 | Dashboards, trends, flakiness, coverage, LLM costs |
|
|
182
|
+
| **Defect Management** | 6 | Create, update, link, sync to Jira/Azure/GitHub |
|
|
183
|
+
| **Webhooks** | 5 | Event subscriptions, delivery management |
|
|
184
|
+
| **Database Admin** | 1 | Alembic migrations (admin only, 2FA required) |
|
|
185
|
+
|
|
186
|
+
### Quick API Examples
|
|
139
187
|
|
|
140
188
|
### Core Workflow
|
|
141
189
|
|
|
@@ -374,6 +422,27 @@ A CI gate is configured in `.github/workflows/quality-gate.yml` to enforce the s
|
|
|
374
422
|
- The API has no authentication by default — add an API key middleware before exposing to a network
|
|
375
423
|
- UI validation only — the agent never makes live requests to the AUT
|
|
376
424
|
|
|
425
|
+
## Change Reports
|
|
426
|
+
|
|
427
|
+
- Commit-range report (`dfdf693b..15328b7`): `docs/changes-dfdf693b-to-15328b7.md`
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Development Roadmap
|
|
432
|
+
|
|
433
|
+
The project follows an **enterprise-grade development roadmap** with 6 sprints covering 190 story points over ~12 weeks. See [docs/todo.md](docs/todo.md) for detailed sprint breakdown with features, user stories, and tasks.
|
|
434
|
+
|
|
435
|
+
### Sprint Summary
|
|
436
|
+
|
|
437
|
+
| Sprint | Focus | Effort | Status |
|
|
438
|
+
|--------|-------|--------|--------|
|
|
439
|
+
| **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
|
|
440
|
+
| **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
|
|
441
|
+
| **Sprint 3** | Durable Execution | 34-40pts | 🔲 Pending (Redis + Celery/Arq setup) |
|
|
442
|
+
| **Sprint 4** | Security & Hardening | 30-36pts | 🔲 Pending (Container sandbox, audit logging) |
|
|
443
|
+
| **Sprint 5** | Reporting & Analytics | 28-34pts | 🔲 Pending (Artifact storage, dashboards) |
|
|
444
|
+
| **Sprint 6** | Advanced Features | 24-30pts | 🔲 Pending (Webhooks, defect sync, versioning) |
|
|
445
|
+
|
|
377
446
|
---
|
|
378
447
|
|
|
379
448
|
## License
|
|
@@ -113,7 +113,28 @@ If local generation feels slow, reduce `OLLAMA_NUM_PREDICT`, keep `SKIP_REVIEW_F
|
|
|
113
113
|
|
|
114
114
|
---
|
|
115
115
|
|
|
116
|
-
##
|
|
116
|
+
## OpenAPI Specification
|
|
117
|
+
|
|
118
|
+
**ScriptGini implements a production-ready OpenAPI 3.0.3 specification** that defines a complete enterprise REST API with 50+ endpoints, 60+ schemas, and comprehensive documentation.
|
|
119
|
+
|
|
120
|
+
### Specification Highlights
|
|
121
|
+
|
|
122
|
+
**The API specification includes 12 salient features**:
|
|
123
|
+
|
|
124
|
+
1. **Multi-tenant Architecture** — Organizations, Workspaces, Teams with hierarchical RBAC
|
|
125
|
+
2. **Advanced LLM Management** — Multi-provider orchestration, health monitoring, cost tracking, and model governance
|
|
126
|
+
3. **Intelligent Test Data Management** — Multi-source ingestion, synthetic generation, PII masking, state locking, placeholder mapping
|
|
127
|
+
4. **Asynchronous Job Management** — HTTP 202 Accepted, idempotent execution, job polling, webhooks
|
|
128
|
+
5. **Comprehensive Reporting** — Structured logs, artifact storage, signed downloads, execution diagnostics
|
|
129
|
+
6. **Analytics & Insights** — Dashboards, trend analysis, flakiness detection, code coverage, LLM usage
|
|
130
|
+
7. **Defect Lifecycle** — Auto-detection, Jira/Azure/GitHub sync, severity tracking, traceability
|
|
131
|
+
8. **Script Versioning** — Git-style history, quality metrics, refactoring, diff metadata
|
|
132
|
+
9. **Webhooks** — Event-driven notifications, delivery guarantees, retry policies
|
|
133
|
+
10. **Security & Authorization** — JWT + OAuth2, API keys with scopes, RBAC enforcement, rate limiting
|
|
134
|
+
11. **Error Handling & Observability** — Standardized error envelope, request ID tracing, correlation IDs
|
|
135
|
+
12. **Pagination & Filtering** — limit/offset/sort, comprehensive filtering, faceted search
|
|
136
|
+
|
|
137
|
+
### API Reference
|
|
117
138
|
|
|
118
139
|
Once running, visit:
|
|
119
140
|
|
|
@@ -122,6 +143,33 @@ Once running, visit:
|
|
|
122
143
|
| `http://localhost:8000/docs` | Swagger UI (interactive) |
|
|
123
144
|
| `http://localhost:8000/redoc` | ReDoc |
|
|
124
145
|
| `http://localhost:8000/health` | Health check |
|
|
146
|
+
| `http://localhost:8000/openapi.json` | Raw OpenAPI 3.0.3 specification |
|
|
147
|
+
|
|
148
|
+
### Full OpenAPI Documentation
|
|
149
|
+
|
|
150
|
+
- **Complete Specification**: [docs/openapi-improved-draft-2026-05-10.yaml](docs/openapi-improved-draft-2026-05-10.yaml)
|
|
151
|
+
- **Feature Documentation**: [docs/OPENAPI-SPECIFICATION.md](docs/OPENAPI-SPECIFICATION.md) — Covers all 12 salient features with examples and flows
|
|
152
|
+
- **API Architecture Review**: [docs/api-architecture-review-2026-05-10.md](docs/api-architecture-review-2026-05-10.md) — Analysis of current state vs. target enterprise API
|
|
153
|
+
|
|
154
|
+
### API Domains (50+ Endpoints)
|
|
155
|
+
|
|
156
|
+
| Domain | Endpoints | Purpose |
|
|
157
|
+
|--------|-----------|---------|
|
|
158
|
+
| **Authentication & IAM** | 8 | Login, token refresh, user management, API keys |
|
|
159
|
+
| **Organizations & Teams** | 6 | Multi-tenancy, team management, RBAC |
|
|
160
|
+
| **Workspaces & Projects** | 5 | Organizational hierarchy, configuration |
|
|
161
|
+
| **Test Data Management** | 11 | Data sets, reservations, masking, synthetic generation |
|
|
162
|
+
| **LLM Management** | 5 | Provider registration, model governance, cost tracking |
|
|
163
|
+
| **Script Engineering** | 7 | Generation, versioning, quality, refactoring |
|
|
164
|
+
| **Test Cases** | 4 | CRUD operations for test definitions |
|
|
165
|
+
| **Test Orchestration** | 5 | Execution, cancellation, job management |
|
|
166
|
+
| **Execution Reporting** | 4 | Reports, logs, artifacts, diagnostics |
|
|
167
|
+
| **Analytics & Insights** | 5 | Dashboards, trends, flakiness, coverage, LLM costs |
|
|
168
|
+
| **Defect Management** | 6 | Create, update, link, sync to Jira/Azure/GitHub |
|
|
169
|
+
| **Webhooks** | 5 | Event subscriptions, delivery management |
|
|
170
|
+
| **Database Admin** | 1 | Alembic migrations (admin only, 2FA required) |
|
|
171
|
+
|
|
172
|
+
### Quick API Examples
|
|
125
173
|
|
|
126
174
|
### Core Workflow
|
|
127
175
|
|
|
@@ -360,6 +408,27 @@ A CI gate is configured in `.github/workflows/quality-gate.yml` to enforce the s
|
|
|
360
408
|
- The API has no authentication by default — add an API key middleware before exposing to a network
|
|
361
409
|
- UI validation only — the agent never makes live requests to the AUT
|
|
362
410
|
|
|
411
|
+
## Change Reports
|
|
412
|
+
|
|
413
|
+
- Commit-range report (`dfdf693b..15328b7`): `docs/changes-dfdf693b-to-15328b7.md`
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Development Roadmap
|
|
418
|
+
|
|
419
|
+
The project follows an **enterprise-grade development roadmap** with 6 sprints covering 190 story points over ~12 weeks. See [docs/todo.md](docs/todo.md) for detailed sprint breakdown with features, user stories, and tasks.
|
|
420
|
+
|
|
421
|
+
### Sprint Summary
|
|
422
|
+
|
|
423
|
+
| Sprint | Focus | Effort | Status |
|
|
424
|
+
|--------|-------|--------|--------|
|
|
425
|
+
| **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
|
|
426
|
+
| **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
|
|
427
|
+
| **Sprint 3** | Durable Execution | 34-40pts | 🔲 Pending (Redis + Celery/Arq setup) |
|
|
428
|
+
| **Sprint 4** | Security & Hardening | 30-36pts | 🔲 Pending (Container sandbox, audit logging) |
|
|
429
|
+
| **Sprint 5** | Reporting & Analytics | 28-34pts | 🔲 Pending (Artifact storage, dashboards) |
|
|
430
|
+
| **Sprint 6** | Advanced Features | 24-30pts | 🔲 Pending (Webhooks, defect sync, versioning) |
|
|
431
|
+
|
|
363
432
|
---
|
|
364
433
|
|
|
365
434
|
## License
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis cache utilities for storing frequently accessed data.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from app.cache import cache
|
|
6
|
+
|
|
7
|
+
# Store value
|
|
8
|
+
cache.set("key", "value", ttl=3600)
|
|
9
|
+
|
|
10
|
+
# Get value
|
|
11
|
+
value = cache.get("key")
|
|
12
|
+
|
|
13
|
+
# Delete value
|
|
14
|
+
cache.delete("key")
|
|
15
|
+
|
|
16
|
+
# Use as decorator
|
|
17
|
+
@cache.cached(ttl=300)
|
|
18
|
+
def expensive_operation():
|
|
19
|
+
return result
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import redis
|
|
24
|
+
from typing import Any, Optional, Callable
|
|
25
|
+
from functools import wraps
|
|
26
|
+
from app.config import settings
|
|
27
|
+
import logging
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RedisCache:
|
|
33
|
+
def __init__(self, redis_url: str):
|
|
34
|
+
"""Initialize Redis connection."""
|
|
35
|
+
try:
|
|
36
|
+
self.redis_client = redis.from_url(redis_url, decode_responses=True)
|
|
37
|
+
# Test connection
|
|
38
|
+
self.redis_client.ping()
|
|
39
|
+
logger.info("Redis cache connected successfully")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Failed to connect to Redis: {e}")
|
|
42
|
+
self.redis_client = None
|
|
43
|
+
|
|
44
|
+
def is_available(self) -> bool:
|
|
45
|
+
"""Check if Redis is available."""
|
|
46
|
+
return self.redis_client is not None
|
|
47
|
+
|
|
48
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Store a value in cache.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
key: Cache key
|
|
54
|
+
value: Value to store (will be JSON serialized if not string)
|
|
55
|
+
ttl: Time to live in seconds (default: configured TTL)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if successful, False otherwise
|
|
59
|
+
"""
|
|
60
|
+
if not self.is_available():
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
ttl = ttl or settings.REDIS_CACHE_TTL_SECONDS
|
|
65
|
+
|
|
66
|
+
if isinstance(value, str):
|
|
67
|
+
self.redis_client.setex(key, ttl, value)
|
|
68
|
+
else:
|
|
69
|
+
self.redis_client.setex(key, ttl, json.dumps(value))
|
|
70
|
+
|
|
71
|
+
return True
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Error setting cache key {key}: {e}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
77
|
+
"""
|
|
78
|
+
Retrieve a value from cache.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
key: Cache key
|
|
82
|
+
default: Default value if key not found or error
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Cached value or default
|
|
86
|
+
"""
|
|
87
|
+
if not self.is_available():
|
|
88
|
+
return default
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
value = self.redis_client.get(key)
|
|
92
|
+
if value is None:
|
|
93
|
+
return default
|
|
94
|
+
|
|
95
|
+
# Try to parse as JSON
|
|
96
|
+
try:
|
|
97
|
+
return json.loads(value)
|
|
98
|
+
except (json.JSONDecodeError, TypeError):
|
|
99
|
+
return value
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error getting cache key {key}: {e}")
|
|
102
|
+
return default
|
|
103
|
+
|
|
104
|
+
def delete(self, key: str) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Delete a value from cache.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
key: Cache key
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if key was deleted, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
if not self.is_available():
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
return bool(self.redis_client.delete(key))
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Error deleting cache key {key}: {e}")
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def clear_pattern(self, pattern: str) -> int:
|
|
124
|
+
"""
|
|
125
|
+
Delete all keys matching a pattern.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
pattern: Key pattern (e.g., "user:*", "script:123:*")
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Number of keys deleted
|
|
132
|
+
"""
|
|
133
|
+
if not self.is_available():
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
keys = self.redis_client.keys(pattern)
|
|
138
|
+
if keys:
|
|
139
|
+
return self.redis_client.delete(*keys)
|
|
140
|
+
return 0
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Error clearing cache pattern {pattern}: {e}")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
def cached(self, ttl: Optional[int] = None):
|
|
146
|
+
"""
|
|
147
|
+
Decorator to cache function results.
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
@cache.cached(ttl=300)
|
|
151
|
+
def expensive_function(arg1, arg2):
|
|
152
|
+
return result
|
|
153
|
+
"""
|
|
154
|
+
def decorator(func: Callable) -> Callable:
|
|
155
|
+
@wraps(func)
|
|
156
|
+
def wrapper(*args, **kwargs):
|
|
157
|
+
# Generate cache key from function name and arguments
|
|
158
|
+
cache_key = f"{func.__module__}:{func.__name__}:{args}:{kwargs}"
|
|
159
|
+
cache_key = cache_key.replace(" ", "") # Remove spaces
|
|
160
|
+
|
|
161
|
+
# Try to get from cache
|
|
162
|
+
cached_value = self.get(cache_key)
|
|
163
|
+
if cached_value is not None:
|
|
164
|
+
logger.debug(f"Cache hit for {cache_key}")
|
|
165
|
+
return cached_value
|
|
166
|
+
|
|
167
|
+
# Call function and cache result
|
|
168
|
+
result = func(*args, **kwargs)
|
|
169
|
+
self.set(cache_key, result, ttl=ttl)
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
return wrapper
|
|
173
|
+
return decorator
|
|
174
|
+
|
|
175
|
+
def increment(self, key: str, amount: int = 1) -> Optional[int]:
|
|
176
|
+
"""Increment a counter."""
|
|
177
|
+
if not self.is_available():
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
return self.redis_client.incrby(key, amount)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Error incrementing {key}: {e}")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def get_ttl(self, key: str) -> Optional[int]:
|
|
187
|
+
"""Get remaining TTL in seconds (-1 if no expiry, -2 if doesn't exist)."""
|
|
188
|
+
if not self.is_available():
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
return self.redis_client.ttl(key)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Error getting TTL for {key}: {e}")
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Initialize cache instance
|
|
199
|
+
cache = RedisCache(settings.REDIS_URL)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from celery import Celery
|
|
2
|
+
from app.config import settings
|
|
3
|
+
|
|
4
|
+
# Initialize Celery app
|
|
5
|
+
celery_app = Celery(
|
|
6
|
+
settings.APP_NAME,
|
|
7
|
+
broker=settings.CELERY_BROKER_URL,
|
|
8
|
+
backend=settings.CELERY_RESULT_BACKEND,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Configure Celery
|
|
12
|
+
celery_app.conf.update(
|
|
13
|
+
task_serializer="json",
|
|
14
|
+
accept_content=["json"],
|
|
15
|
+
result_serializer="json",
|
|
16
|
+
timezone="UTC",
|
|
17
|
+
enable_utc=True,
|
|
18
|
+
task_track_started=True,
|
|
19
|
+
task_time_limit=30 * 60, # 30 minute hard time limit
|
|
20
|
+
task_soft_time_limit=25 * 60, # 25 minute soft time limit
|
|
21
|
+
worker_prefetch_multiplier=4,
|
|
22
|
+
worker_max_tasks_per_child=1000,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@celery_app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
|
|
27
|
+
def debug_task(self):
|
|
28
|
+
"""Test task to verify Celery is working."""
|
|
29
|
+
print(f"Request: {self.request!r}")
|
|
30
|
+
return "Celery is working!"
|
|
@@ -9,8 +9,24 @@ class Settings(BaseSettings):
|
|
|
9
9
|
APP_NAME: str = "ScriptGini"
|
|
10
10
|
DEBUG: bool = False
|
|
11
11
|
|
|
12
|
-
# Database
|
|
13
|
-
|
|
12
|
+
# Database (PostgreSQL)
|
|
13
|
+
# Format: postgresql://user:password@host:port/dbname
|
|
14
|
+
DATABASE_URL: str = "postgresql://scriptgini:scriptgini@localhost:5433/scriptgini_db"
|
|
15
|
+
DATABASE_ECHO: bool = False # Set to True to log SQL queries
|
|
16
|
+
|
|
17
|
+
# Redis (Task Queue & Caching)
|
|
18
|
+
REDIS_URL: str = "redis://localhost:6379/0"
|
|
19
|
+
REDIS_CACHE_TTL_SECONDS: int = 3600 # 1 hour default cache TTL
|
|
20
|
+
|
|
21
|
+
# Celery Task Queue
|
|
22
|
+
CELERY_BROKER_URL: str = "redis://localhost:6379/1" # Different DB for task queue
|
|
23
|
+
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2" # Different DB for results
|
|
24
|
+
CELERY_TASK_TIMEOUT: int = 600 # 10 minutes default task timeout
|
|
25
|
+
CELERY_MAX_RETRIES: int = 3
|
|
26
|
+
JWT_SECRET_KEY: str = "your-secret-key-change-in-production"
|
|
27
|
+
JWT_ALGORITHM: str = "HS256"
|
|
28
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
29
|
+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
|
14
30
|
|
|
15
31
|
# Default LLM provider
|
|
16
32
|
DEFAULT_LLM_PROVIDER: Literal["openai", "ollama", "openrouter", "gemini", "bedrock"] = "openai"
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
from sqlalchemy import create_engine
|
|
1
|
+
from sqlalchemy import create_engine, event
|
|
2
2
|
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
|
3
3
|
|
|
4
4
|
from app.config import settings
|
|
5
5
|
|
|
6
|
+
# PostgreSQL engine configuration
|
|
6
7
|
engine = create_engine(
|
|
7
8
|
settings.DATABASE_URL,
|
|
8
|
-
|
|
9
|
+
echo=settings.DATABASE_ECHO,
|
|
10
|
+
pool_size=20,
|
|
11
|
+
max_overflow=40,
|
|
12
|
+
pool_pre_ping=True, # Verify connections before using them
|
|
13
|
+
pool_recycle=3600, # Recycle connections after 1 hour
|
|
9
14
|
)
|
|
10
15
|
|
|
11
16
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from fastapi import FastAPI
|
|
@@ -6,22 +7,38 @@ from fastapi.responses import FileResponse
|
|
|
6
7
|
from fastapi.middleware.cors import CORSMiddleware
|
|
7
8
|
from fastapi.staticfiles import StaticFiles
|
|
8
9
|
|
|
10
|
+
from app import __version__
|
|
9
11
|
from app.config import settings
|
|
10
12
|
from app.llm.provider import get_llm_diagnostics
|
|
11
|
-
from app.routers import projects, test_cases, scripts, bulk_jobs, analytics, demo
|
|
13
|
+
from app.routers import projects, test_cases, scripts, bulk_jobs, analytics, demo, auth, api_key, organizations
|
|
12
14
|
|
|
13
15
|
logging.basicConfig(level=logging.DEBUG if settings.DEBUG else logging.INFO)
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
static_dir = Path(__file__).resolve().parent / "static"
|
|
17
19
|
|
|
20
|
+
|
|
21
|
+
@asynccontextmanager
|
|
22
|
+
async def lifespan(_: FastAPI):
|
|
23
|
+
diagnostics = get_llm_diagnostics()
|
|
24
|
+
logger.info(
|
|
25
|
+
"Runtime LLM default: provider=%s model=%s api_key_env=%s api_key_present=%s api_key=%s",
|
|
26
|
+
diagnostics["provider"],
|
|
27
|
+
diagnostics["model"],
|
|
28
|
+
diagnostics["api_key_env"],
|
|
29
|
+
diagnostics["api_key_present"],
|
|
30
|
+
diagnostics["api_key_masked"],
|
|
31
|
+
)
|
|
32
|
+
yield
|
|
33
|
+
|
|
18
34
|
app = FastAPI(
|
|
19
35
|
title=settings.APP_NAME,
|
|
20
36
|
description=(
|
|
21
37
|
"Enterprise-grade Agentic AI system that converts functional test cases "
|
|
22
38
|
"into high-quality automation scripts."
|
|
23
39
|
),
|
|
24
|
-
version=
|
|
40
|
+
version=__version__,
|
|
41
|
+
lifespan=lifespan,
|
|
25
42
|
)
|
|
26
43
|
|
|
27
44
|
app.add_middleware(
|
|
@@ -38,22 +55,12 @@ app.include_router(scripts.router, prefix="/api/v1")
|
|
|
38
55
|
app.include_router(bulk_jobs.router, prefix="/api/v1")
|
|
39
56
|
app.include_router(analytics.router, prefix="/api/v1")
|
|
40
57
|
app.include_router(demo.router, prefix="/api/v1")
|
|
58
|
+
app.include_router(auth.router, prefix="/api/v1")
|
|
59
|
+
app.include_router(api_key.router, prefix="/api/v1")
|
|
60
|
+
app.include_router(organizations.router, prefix="/api/v1")
|
|
41
61
|
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
42
62
|
|
|
43
63
|
|
|
44
|
-
@app.on_event("startup")
|
|
45
|
-
def log_llm_runtime_config() -> None:
|
|
46
|
-
diagnostics = get_llm_diagnostics()
|
|
47
|
-
logger.info(
|
|
48
|
-
"Runtime LLM default: provider=%s model=%s api_key_env=%s api_key_present=%s api_key=%s",
|
|
49
|
-
diagnostics["provider"],
|
|
50
|
-
diagnostics["model"],
|
|
51
|
-
diagnostics["api_key_env"],
|
|
52
|
-
diagnostics["api_key_present"],
|
|
53
|
-
diagnostics["api_key_masked"],
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
64
|
@app.get("/api/v1/runtime/llm", tags=["Runtime"])
|
|
58
65
|
def runtime_llm():
|
|
59
66
|
default_diagnostics = get_llm_diagnostics()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Optional, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import String, DateTime, ForeignKey, JSON, Boolean
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
|
+
|
|
7
|
+
from app.database import Base
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from app.models.user import User
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APIKey(Base):
|
|
14
|
+
__tablename__ = "api_keys"
|
|
15
|
+
|
|
16
|
+
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
17
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
|
18
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
19
|
+
prefix: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
20
|
+
hashed_secret: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
21
|
+
scopes: Mapped[list] = mapped_column(JSON, default=list, nullable=False) # e.g., ["read", "write", "execute"]
|
|
22
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
23
|
+
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
24
|
+
last_used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
25
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
26
|
+
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
27
|
+
)
|
|
28
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
29
|
+
DateTime(timezone=True),
|
|
30
|
+
default=lambda: datetime.now(timezone.utc),
|
|
31
|
+
onupdate=lambda: datetime.now(timezone.utc),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Relationship
|
|
35
|
+
user: Mapped["User"] = relationship(back_populates="api_keys")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import DateTime, Boolean, ForeignKey, Enum as SAEnum, UniqueConstraint
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
|
+
|
|
7
|
+
from app.database import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Role(str, enum.Enum):
|
|
11
|
+
owner = "owner"
|
|
12
|
+
admin = "admin"
|
|
13
|
+
member = "member"
|
|
14
|
+
viewer = "viewer"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OrganizationMembership(Base):
|
|
18
|
+
__tablename__ = "organization_memberships"
|
|
19
|
+
__table_args__ = (UniqueConstraint("organization_id", "user_id", name="uq_org_membership_org_user"),)
|
|
20
|
+
|
|
21
|
+
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
22
|
+
organization_id: Mapped[int] = mapped_column(ForeignKey("organizations.id"), nullable=False, index=True)
|
|
23
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
|
24
|
+
role: Mapped[Role] = mapped_column(SAEnum(Role), nullable=False, default=Role.member)
|
|
25
|
+
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
26
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
27
|
+
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
28
|
+
)
|
|
29
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
30
|
+
DateTime(timezone=True),
|
|
31
|
+
default=lambda: datetime.now(timezone.utc),
|
|
32
|
+
onupdate=lambda: datetime.now(timezone.utc),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ProjectMembership(Base):
|
|
38
|
+
__tablename__ = "project_memberships"
|
|
39
|
+
__table_args__ = (UniqueConstraint("project_id", "user_id", name="uq_project_membership_project_user"),)
|
|
40
|
+
|
|
41
|
+
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
42
|
+
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True)
|
|
43
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
|
44
|
+
role: Mapped[Role] = mapped_column(SAEnum(Role), nullable=False, default=Role.member)
|
|
45
|
+
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
46
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
47
|
+
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
48
|
+
)
|
|
49
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
50
|
+
DateTime(timezone=True),
|
|
51
|
+
default=lambda: datetime.now(timezone.utc),
|
|
52
|
+
onupdate=lambda: datetime.now(timezone.utc),
|
|
53
|
+
)
|
|
54
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import String, Text, DateTime, ForeignKey
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
|
|
6
|
+
from app.database import Base
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Organization(Base):
|
|
10
|
+
__tablename__ = "organizations"
|
|
11
|
+
|
|
12
|
+
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
13
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
|
|
14
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
15
|
+
created_by_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
|
16
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
17
|
+
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
18
|
+
)
|
|
19
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
20
|
+
DateTime(timezone=True),
|
|
21
|
+
default=lambda: datetime.now(timezone.utc),
|
|
22
|
+
onupdate=lambda: datetime.now(timezone.utc),
|
|
23
|
+
)
|
|
24
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
3
|
|
|
4
|
-
from sqlalchemy import String, Text, DateTime, Enum as SAEnum
|
|
4
|
+
from sqlalchemy import String, Text, DateTime, Enum as SAEnum, ForeignKey
|
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
6
|
|
|
7
7
|
from app.database import Base
|
|
@@ -26,6 +26,7 @@ class Project(Base):
|
|
|
26
26
|
__tablename__ = "projects"
|
|
27
27
|
|
|
28
28
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
29
|
+
organization_id: Mapped[int | None] = mapped_column(ForeignKey("organizations.id"), nullable=True, index=True)
|
|
29
30
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
30
31
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
31
32
|
aut_base_url: Mapped[str] = mapped_column(String(2048), nullable=False)
|
|
@@ -44,3 +45,4 @@ class Project(Base):
|
|
|
44
45
|
default=lambda: datetime.now(timezone.utc),
|
|
45
46
|
onupdate=lambda: datetime.now(timezone.utc),
|
|
46
47
|
)
|
|
48
|
+
|