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.
Files changed (62) hide show
  1. {scriptgini-1.0.6 → scriptgini-1.2.0}/PKG-INFO +71 -2
  2. {scriptgini-1.0.6 → scriptgini-1.2.0}/README.md +70 -1
  3. scriptgini-1.2.0/app/__init__.py +3 -0
  4. scriptgini-1.2.0/app/cache.py +199 -0
  5. scriptgini-1.2.0/app/celery_app.py +30 -0
  6. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/config.py +18 -2
  7. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/database.py +7 -2
  8. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/main.py +22 -15
  9. scriptgini-1.2.0/app/models/api_key.py +35 -0
  10. scriptgini-1.2.0/app/models/membership.py +54 -0
  11. scriptgini-1.2.0/app/models/organization.py +24 -0
  12. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/project.py +3 -1
  13. scriptgini-1.2.0/app/models/user.py +32 -0
  14. scriptgini-1.2.0/app/routers/api_key.py +126 -0
  15. scriptgini-1.2.0/app/routers/auth.py +114 -0
  16. scriptgini-1.2.0/app/routers/organizations.py +36 -0
  17. scriptgini-1.2.0/app/routers/projects.py +113 -0
  18. scriptgini-1.2.0/app/schemas/api_key.py +76 -0
  19. scriptgini-1.2.0/app/schemas/auth.py +43 -0
  20. scriptgini-1.2.0/app/schemas/membership.py +22 -0
  21. scriptgini-1.2.0/app/schemas/organization.py +19 -0
  22. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/project.py +3 -0
  23. scriptgini-1.2.0/app/services/api_key.py +179 -0
  24. scriptgini-1.2.0/app/services/auth.py +101 -0
  25. scriptgini-1.2.0/app/services/auth_dependencies.py +110 -0
  26. scriptgini-1.2.0/app/services/rbac.py +118 -0
  27. scriptgini-1.2.0/app/tasks.py +138 -0
  28. {scriptgini-1.0.6 → scriptgini-1.2.0}/pyproject.toml +8 -1
  29. {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/PKG-INFO +71 -2
  30. {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/SOURCES.txt +22 -1
  31. {scriptgini-1.0.6 → scriptgini-1.2.0}/tests/test_api.py +1 -0
  32. scriptgini-1.2.0/tests/test_auth.py +437 -0
  33. {scriptgini-1.0.6 → scriptgini-1.2.0}/tests/test_coverage.py +1 -0
  34. scriptgini-1.2.0/tests/test_infra_services_coverage.py +439 -0
  35. scriptgini-1.2.0/tests/test_sprint2_rbac.py +295 -0
  36. scriptgini-1.0.6/app/routers/projects.py +0 -51
  37. scriptgini-1.0.6/app/schemas/__init__.py +0 -0
  38. {scriptgini-1.0.6/app → scriptgini-1.2.0/app/agents}/__init__.py +0 -0
  39. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/agents/prompts.py +0 -0
  40. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/agents/script_gini_agent.py +0 -0
  41. {scriptgini-1.0.6/app/agents → scriptgini-1.2.0/app/llm}/__init__.py +0 -0
  42. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/llm/provider.py +0 -0
  43. {scriptgini-1.0.6/app/llm → scriptgini-1.2.0/app/models}/__init__.py +0 -0
  44. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/bulk_job.py +0 -0
  45. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/generated_script.py +0 -0
  46. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/script_run.py +0 -0
  47. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/models/test_case.py +0 -0
  48. {scriptgini-1.0.6/app/models → scriptgini-1.2.0/app/routers}/__init__.py +0 -0
  49. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/analytics.py +0 -0
  50. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/bulk_jobs.py +0 -0
  51. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/demo.py +0 -0
  52. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/scripts.py +0 -0
  53. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/routers/test_cases.py +0 -0
  54. {scriptgini-1.0.6/app/routers → scriptgini-1.2.0/app/schemas}/__init__.py +0 -0
  55. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/analytics.py +0 -0
  56. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/bulk_job.py +0 -0
  57. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/generated_script.py +0 -0
  58. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/schemas/test_case.py +0 -0
  59. {scriptgini-1.0.6 → scriptgini-1.2.0}/app/services/git_export.py +0 -0
  60. {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/dependency_links.txt +0 -0
  61. {scriptgini-1.0.6 → scriptgini-1.2.0}/scriptgini.egg-info/top_level.txt +0 -0
  62. {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.6
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
- ## API Reference
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
- ## API Reference
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,3 @@
1
+ __version__ = "1.2.0"
2
+ __api_version__ = "v1.2.0"
3
+
@@ -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
- DATABASE_URL: str = "sqlite:///./scriptgini.db"
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
- connect_args={"check_same_thread": False}, # required for SQLite
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="1.0.6",
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
+