abrar6024 commited on
Commit
76b843b
·
0 Parent(s):

Clean repo without venv

Browse files
Files changed (11) hide show
  1. .gitattributes +35 -0
  2. .gitignore +4 -0
  3. Dockerfile +32 -0
  4. README.md +210 -0
  5. inference.py +156 -0
  6. openenv.yaml +59 -0
  7. pyproject.toml +19 -0
  8. requirements.txt +6 -0
  9. server/app.py +724 -0
  10. sql_debugger_env.py +613 -0
  11. uv.lock +323 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ venv/
2
+ venv311/
3
+ __pycache__/
4
+ *.pyc
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ LABEL maintainer="NeuroHack"
4
+ LABEL description="SQL Debugger & Optimizer — OpenEnv"
5
+
6
+ WORKDIR /app
7
+
8
+ RUN apt-get update && apt-get install -y --no-install-recommends curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy dependency files
12
+ COPY requirements.txt .
13
+ COPY pyproject.toml .
14
+ COPY uv.lock .
15
+
16
+ # Install dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy project structure
20
+ COPY server/ ./server/
21
+ COPY sql_debugger_env.py .
22
+ COPY openenv.yaml .
23
+ COPY inference.py .
24
+ COPY README.md .
25
+
26
+ EXPOSE 7860
27
+
28
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
29
+ CMD curl -f http://localhost:7860/health || exit 1
30
+
31
+ # 🔥 IMPORTANT CHANGE
32
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SQL Debugger & Optimizer
3
+ emoji: 🛠️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # 🛠️ SQL Debugger & Optimizer — OpenEnv Environment
12
+
13
+ > 🚀 **NeuroHack — OpenEnv Submission**
14
+ > A real-world reinforcement learning environment where an AI agent debugs broken SQL queries using **deterministic SQLite execution** — no LLM-based scoring.
15
+
16
+ ---
17
+
18
+ ## 📋 Table of Contents
19
+
20
+ - [Overview](#overview)
21
+ - [Why This Wins](#why-this-wins)
22
+ - [Live Demo](#live-demo)
23
+ - [Project Structure](#project-structure)
24
+ - [Getting Started](#getting-started)
25
+ - [Run Locally](#run-locally)
26
+ - [Docker](#docker)
27
+ - [API Example](#api-example)
28
+ - [Environment Details](#environment-details)
29
+ - [Tasks](#tasks)
30
+ - [Reward System](#reward-system)
31
+ - [Performance](#performance)
32
+ - [License](#license)
33
+
34
+ ---
35
+
36
+ ## Overview
37
+
38
+ The **SQL Debugger & Optimizer** is an OpenEnv-compatible reinforcement learning environment where an AI agent receives broken SQL queries and must output corrected versions.
39
+
40
+ The agent receives:
41
+ - 🔴 A **broken SQL query**
42
+ - 🗂️ The **database schema**
43
+ - 📝 A **natural language description** of the intended behavior
44
+
45
+ It must output a **corrected SQL query**, which is then:
46
+ 1. Executed against a real SQLite database
47
+ 2. Compared against the reference (correct) query output
48
+ 3. Scored with a layered reward from `0.0 → 1.0`
49
+
50
+ ---
51
+
52
+ ## Why This Wins
53
+
54
+ Unlike traditional LLM-evaluated systems:
55
+
56
+ | Feature | Description |
57
+ |--------|-------------|
58
+ | ✅ **100% Deterministic Grading** | Real SQLite execution — no subjective LLM scoring |
59
+ | ✅ **Layered Reward System** | Syntax → Logic → Data → Optimization |
60
+ | ✅ **Real-World Bugs** | JOIN errors, N+1 queries, SQL injection vulnerabilities |
61
+ | ✅ **Engineering Relevance** | Mirrors production database debugging scenarios |
62
+ | ✅ **OpenEnv-Compatible API** | Drop-in `/reset` and `/step` endpoints |
63
+
64
+ ---
65
+
66
+ ## Live Demo
67
+
68
+ Open the interactive UI at:
69
+
70
+ ```
71
+ http://localhost:7860/ui
72
+ ```
73
+
74
+ With the UI you can:
75
+ - Fix broken SQL queries interactively
76
+ - View the corrected SQL output
77
+ - See reward scores in real-time
78
+ - Track agent performance with graphs
79
+
80
+ ---
81
+
82
+ ## Project Structure
83
+
84
+ ```
85
+ sql-debugger-env/
86
+ ├── app.py # FastAPI server — /reset, /step, /ui endpoints
87
+ ├── sql_debugger_env.py # Core RL environment logic & SQLite execution
88
+ ├── inference.py # Agent inference utilities
89
+ ├── openenv.yaml # OpenEnv environment specification
90
+ ├── requirements.txt # Python dependencies
91
+ ├── Dockerfile # Container build configuration
92
+ └── README.md # This file
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Getting Started
98
+
99
+ ### Run Locally
100
+
101
+ ```bash
102
+ pip install -r requirements.txt
103
+ python app.py
104
+ ```
105
+
106
+ Then open: [http://localhost:7860/ui](http://localhost:7860/ui)
107
+
108
+ ---
109
+
110
+ ### Docker
111
+
112
+ ```bash
113
+ # Build the image
114
+ docker build -t sql-debugger-env .
115
+
116
+ # Run the container
117
+ docker run -p 7860:7860 sql-debugger-env
118
+ ```
119
+
120
+ ---
121
+
122
+ ### API Example
123
+
124
+ The environment exposes an OpenEnv-compatible REST API:
125
+
126
+ ```python
127
+ import requests
128
+
129
+ # 1. Start a new episode
130
+ r = requests.post("http://localhost:7860/reset", json={"task": "medium"})
131
+ session_id = r.json()["session_id"]
132
+ obs = r.json()["observation"]
133
+
134
+ # 2. Submit a fix action
135
+ action = {
136
+ "challenge_id": obs["challenge"]["id"],
137
+ "fixed_sql": "SELECT users.id, orders.total FROM users JOIN orders ON users.id = orders.user_id",
138
+ "explanation": "Fixed JOIN logic — was using wrong foreign key",
139
+ "detected_issues": ["wrong_join"]
140
+ }
141
+
142
+ r = requests.post("http://localhost:7860/step", json={
143
+ "session_id": session_id,
144
+ "action": action
145
+ })
146
+
147
+ print(r.json()["reward"]) # e.g. 0.92
148
+ ```
149
+
150
+ **Endpoints:**
151
+
152
+ | Method | Endpoint | Description |
153
+ |--------|----------|-------------|
154
+ | `POST` | `/reset` | Start a new episode. Accepts `{"task": "easy" \| "medium" \| "hard"}` |
155
+ | `POST` | `/step` | Submit a fix. Returns reward, done flag, and next observation |
156
+ | `GET` | `/ui` | Interactive web interface |
157
+
158
+ ---
159
+
160
+ ## Environment Details
161
+
162
+ ### Tasks
163
+
164
+ | Difficulty | Bug Types |
165
+ |------------|-----------|
166
+ | 🟢 **Easy** | Syntax errors (`SELCT`, `FORM`, missing `WHERE`) |
167
+ | 🟡 **Medium** | JOIN logic errors, aggregation mistakes |
168
+ | 🔴 **Hard** | N+1 query patterns, optimization issues, complex aggregations |
169
+
170
+ ---
171
+
172
+ ### Reward System
173
+
174
+ Each submitted query is evaluated across multiple components:
175
+
176
+ | Component | Weight | Description |
177
+ |-----------|--------|-------------|
178
+ | Syntax Correctness | `0.20` | Query parses and executes without error |
179
+ | Row Count Match | `0.25` | Output row count matches reference |
180
+ | Column Match | `0.15` | Returned columns match expected schema |
181
+ | Data Exact Match | `0.30` | Row-by-row data comparison |
182
+ | Security Fix | `0.10` | SQL injection or unsafe patterns resolved |
183
+ | Optimization | `0.05` | Query avoids N+1 or redundant scans |
184
+ | Explanation Quality | `0.05` | Detected issues and explanation provided |
185
+ | **Total** | **1.00** | |
186
+
187
+ ---
188
+
189
+ ### Performance
190
+
191
+ Benchmarks from the reference agent (`inference.py`):
192
+
193
+ | Task | Score |
194
+ |------|-------|
195
+ | Easy | ~0.90 |
196
+ | Medium | ~0.90 |
197
+ | Hard | ~0.76 |
198
+ | **Average** | 🚀 **~0.85** |
199
+
200
+ ---
201
+
202
+ ## License
203
+
204
+ This project is licensed under the **MIT License** — see [`LICENSE`](LICENSE) for details.
205
+
206
+ ---
207
+
208
+ <div align="center">
209
+ Built for <strong>NeuroHack</strong> · OpenEnv Track · 2025
210
+ </div>
inference.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ inference.py — SQL Debugger & Optimizer Agent
3
+ ===============================================
4
+ Runs an LLM against all 3 tasks. Emits mandatory [START]/[STEP]/[END] logs.
5
+
6
+ Env vars:
7
+ API_BASE_URL LLM endpoint (default: https://router.huggingface.co/v1)
8
+ MODEL_NAME Model name (default: Qwen/Qwen2.5-72B-Instruct)
9
+ HF_TOKEN API key
10
+ """
11
+ from __future__ import annotations
12
+ import json, os, textwrap
13
+ from typing import Dict, List, Optional
14
+ from openai import OpenAI
15
+ from sql_debugger_env import Action, SQLDebuggerEnv
16
+
17
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "")
18
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
19
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
20
+ BENCHMARK = "sql-debugger-agent"
21
+ TASKS = ["easy", "medium", "hard"]
22
+
23
+ client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)
24
+
25
+ SYSTEM_PROMPT = textwrap.dedent("""
26
+ You are an expert SQL debugger and optimizer.
27
+ You will be given a broken SQL query, the database schema, and a description
28
+ of what the query SHOULD do. Your job is to fix ALL bugs.
29
+
30
+ Always respond with ONLY a JSON object — no markdown, no explanation outside JSON:
31
+ {
32
+ "challenge_id": "<id>",
33
+ "fixed_sql": "<your corrected SQL query>",
34
+ "explanation": "<brief explanation of what was wrong>",
35
+ "detected_issues": ["issue1", "issue2"]
36
+ }
37
+
38
+ Common bug types to look for:
39
+ - Syntax typos (SELCT, FORM, WHER)
40
+ - Missing GROUP BY when using aggregate functions with non-aggregate columns
41
+ - Wrong JOIN type (INNER vs LEFT)
42
+ - Wrong column in ORDER BY (ordering by raw column instead of aggregate alias)
43
+ - Subquery referencing wrong scope (AVG of all rows instead of filtered group)
44
+ - SQL injection (string concatenation of user input — fix with parameterized form)
45
+ - N+1 correlated subqueries (rewrite as JOIN with subquery or CTE)
46
+
47
+ Output ONLY the JSON. Nothing else.
48
+ """).strip()
49
+
50
+
51
+ def build_prompt(obs_dict: dict) -> str:
52
+ c = obs_dict["challenge"]
53
+ return textwrap.dedent(f"""
54
+ TASK: {obs_dict['task']}
55
+ INSTRUCTIONS: {obs_dict['instructions']}
56
+
57
+ DATABASE SCHEMA:
58
+ {obs_dict['schema_info']}
59
+
60
+ CHALLENGE ID: {c['id']}
61
+ GOAL: {c['description']}
62
+ DIFFICULTY: {c['difficulty']}
63
+
64
+ KNOWN BUGS (hints):
65
+ {chr(10).join('- ' + b for b in c['bugs'])}
66
+
67
+ HINT: {c['hint']}
68
+
69
+ BROKEN SQL TO FIX:
70
+ {c['broken_sql']}
71
+
72
+ Output only JSON with keys: challenge_id, fixed_sql, explanation, detected_issues
73
+ """).strip()
74
+
75
+
76
+ def run_task(task_name: str) -> dict:
77
+ env = SQLDebuggerEnv(task=task_name)
78
+ obs_obj = env.reset()
79
+ obs = obs_obj.model_dump()
80
+
81
+ step_num = 0
82
+ rewards: List[float] = []
83
+ done = False
84
+ last_error = None
85
+
86
+ print(f"[START] task={task_name} env={BENCHMARK} model={MODEL_NAME}", flush=True)
87
+
88
+ while not done:
89
+ prompt = build_prompt(obs)
90
+ try:
91
+ resp = client.chat.completions.create(
92
+ model=MODEL_NAME,
93
+ messages=[
94
+ {"role": "system", "content": SYSTEM_PROMPT},
95
+ {"role": "user", "content": prompt},
96
+ ],
97
+ max_tokens=600,
98
+ temperature=0.1,
99
+ )
100
+ raw = resp.choices[0].message.content or ""
101
+ raw = raw.strip().strip("```json").strip("```").strip()
102
+ action_dict = json.loads(raw)
103
+ action = Action(**action_dict)
104
+ last_error = None
105
+ except Exception as e:
106
+ last_error = str(e)[:100]
107
+ cid = obs["challenge"]["id"]
108
+ action = Action(
109
+ challenge_id=cid,
110
+ fixed_sql=obs["challenge"]["broken_sql"],
111
+ explanation="parse error fallback",
112
+ detected_issues=[],
113
+ )
114
+
115
+ obs_obj, reward, done, info = env.step(action)
116
+ obs = obs_obj.model_dump()
117
+ step_num += 1
118
+ rewards.append(reward)
119
+
120
+ action_str = f"fix(id={action.challenge_id},issues={len(action.detected_issues)})"
121
+ print(
122
+ f"[STEP] step={step_num} action={action_str} "
123
+ f"reward={reward:.2f} done={str(done).lower()} "
124
+ f"error={last_error if last_error else 'null'}",
125
+ flush=True,
126
+ )
127
+
128
+ score = env.episode_score()
129
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
130
+ print(
131
+ f"[END] success={str(score >= 0.5).lower()} steps={step_num} "
132
+ f"score={score:.2f} rewards={rewards_str}",
133
+ flush=True,
134
+ )
135
+ env.close()
136
+ return {"task": task_name, "score": score, "steps": step_num}
137
+
138
+
139
+ def main():
140
+ print(f"# SQL Debugger & Optimizer — Baseline Inference", flush=True)
141
+ print(f"# Model: {MODEL_NAME} API: {API_BASE_URL}\n", flush=True)
142
+ results = []
143
+ for task in TASKS:
144
+ result = run_task(task)
145
+ results.append(result)
146
+ print(f"# Task '{task}' → score: {result['score']:.4f}\n", flush=True)
147
+
148
+ avg = sum(r["score"] for r in results) / len(results)
149
+ print("# === FINAL SUMMARY ===", flush=True)
150
+ for r in results:
151
+ print(f"# {r['task']:8s}: {r['score']:.4f}", flush=True)
152
+ print(f"# AVERAGE : {avg:.4f}", flush=True)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
openenv.yaml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: sql-debugger-agent
2
+ version: "1.0.0"
3
+ description: >
4
+ A real-world environment where an AI agent debugs broken SQL queries —
5
+ fixing syntax errors, logic bugs (wrong JOINs, aggregation mistakes,
6
+ subquery scope), performance anti-patterns (N+1 correlated subqueries),
7
+ and SQL injection vulnerabilities. Uses a live in-memory SQLite database
8
+ for 100% deterministic, execution-based grading.
9
+
10
+ author: NeuroHack
11
+ tags:
12
+ - openenv
13
+ - sql
14
+ - debugging
15
+ - optimization
16
+ - security
17
+ - real-world
18
+ - data-engineering
19
+
20
+ tasks:
21
+ - name: easy
22
+ description: Fix SQL syntax errors — typos in keywords, missing clauses.
23
+ difficulty: easy
24
+ max_steps: 3
25
+ reward_range: [0.0, 1.0]
26
+
27
+ - name: medium
28
+ description: Fix logic bugs — wrong JOIN types, incorrect aggregation scope, bad ORDER BY.
29
+ difficulty: medium
30
+ max_steps: 3
31
+ reward_range: [0.0, 1.0]
32
+
33
+ - name: hard
34
+ description: >
35
+ Fix + optimize + secure: N+1 correlated subqueries, SQL injection vulnerabilities,
36
+ and missing column aliases. All bugs must be identified and fixed.
37
+ difficulty: hard
38
+ max_steps: 3
39
+ reward_range: [0.0, 1.0]
40
+
41
+ interface:
42
+ observation: Observation
43
+ action: Action
44
+ reward: Reward
45
+ methods:
46
+ - reset
47
+ - step
48
+ - state
49
+
50
+ environment:
51
+ python: ">=3.10"
52
+ framework: openenv-core
53
+ database: sqlite3 (in-memory, no external deps)
54
+ api: openai-compatible
55
+
56
+ hf_space:
57
+ sdk: docker
58
+ tags:
59
+ - openenv
pyproject.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "sql-debugger-env"
3
+ version = "0.1.0"
4
+ description = "SQL Debugger & Optimizer OpenEnv environment"
5
+ authors = [{ name = "Abrar" }]
6
+
7
+ dependencies = [
8
+ "fastapi",
9
+ "uvicorn",
10
+ "pydantic",
11
+ "openenv"
12
+ ]
13
+ requires-python = ">=3.10,<3.12"
14
+ [project.scripts]
15
+ server = "server.app:main"
16
+
17
+ [build-system]
18
+ requires = ["setuptools", "wheel"]
19
+ build-backend = "setuptools.build_meta"
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi>=0.110.0
2
+ uvicorn[standard]>=0.29.0
3
+ pydantic>=2.6.0
4
+ openai>=1.14.0
5
+ pyyaml>=6.0
6
+ httpx>=0.27.0
server/app.py ADDED
@@ -0,0 +1,724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SQL Debugger & Optimizer — FastAPI Server for HF Spaces
3
+ """
4
+ from __future__ import annotations
5
+ import os
6
+ import uuid
7
+ from typing import Any, Dict, Optional
8
+
9
+ from fastapi import FastAPI, HTTPException, Body
10
+ from fastapi.responses import HTMLResponse
11
+ from pydantic import BaseModel
12
+
13
+ from sql_debugger_env import Action, SQLDebuggerEnv
14
+
15
+ app = FastAPI(
16
+ title="SQL Debugger & Optimizer — OpenEnv",
17
+ description="Real-world SQL debugging environment for RL agent evaluation.",
18
+ version="1.0.0",
19
+ )
20
+
21
+ _sessions: Dict[str, SQLDebuggerEnv] = {}
22
+
23
+
24
+ class ResetRequest(BaseModel):
25
+ task: str = "easy"
26
+ session_id: Optional[str] = None
27
+
28
+
29
+ class StepRequest(BaseModel):
30
+ session_id: str
31
+ action: Dict[str, Any]
32
+
33
+
34
+ # ✅ HEALTH CHECK
35
+ @app.get("/health")
36
+ def health():
37
+ return {"status": "ok"}
38
+
39
+
40
+ # ✅ RESET
41
+ @app.post("/reset")
42
+ def reset(req: Optional[ResetRequest] = Body(default=None)):
43
+ task = req.task if req else "easy"
44
+ session_id = req.session_id if req else str(uuid.uuid4())
45
+
46
+ env = SQLDebuggerEnv(task=task)
47
+ _sessions[session_id] = env
48
+ obs = env.reset()
49
+
50
+ return {
51
+ "session_id": session_id,
52
+ "observation": obs.model_dump()
53
+ }
54
+
55
+
56
+ # ✅ STEP
57
+ @app.post("/step")
58
+ def step(req: StepRequest):
59
+ env = _sessions.get(req.session_id)
60
+ if not env:
61
+ raise HTTPException(status_code=404, detail="Session not found.")
62
+
63
+ action = Action(**req.action)
64
+ obs, reward, done, info = env.step(action)
65
+
66
+ return {
67
+ "observation": obs.model_dump(),
68
+ "reward": reward,
69
+ "done": done,
70
+ "info": info
71
+ }
72
+
73
+
74
+ # ✅ ROOT UI
75
+ @app.get("/", response_class=HTMLResponse)
76
+ def ui():
77
+ return r"""<!DOCTYPE html>
78
+ <html lang="en">
79
+ <head>
80
+ <meta charset="UTF-8"/>
81
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
82
+ <title>SQL Debugger & Optimizer</title>
83
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@700;800&display=swap" rel="stylesheet"/>
84
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
85
+ <style>
86
+ :root {
87
+ --bg: #080c14;
88
+ --surface: #0d1525;
89
+ --card: #111b2e;
90
+ --border: #1e2f4a;
91
+ --accent: #00d4ff;
92
+ --accent2: #7c3aed;
93
+ --green: #22d3a0;
94
+ --yellow: #fbbf24;
95
+ --red: #f87171;
96
+ --text: #e2eaf8;
97
+ --muted: #5a7092;
98
+ --glow: 0 0 24px rgba(0,212,255,.25);
99
+ }
100
+ *{box-sizing:border-box;margin:0;padding:0;}
101
+ html,body{height:100%;background:var(--bg);color:var(--text);font-family:'JetBrains Mono',monospace;}
102
+
103
+ /* ── Background grid ── */
104
+ body::before{
105
+ content:'';position:fixed;inset:0;z-index:0;
106
+ background-image:
107
+ linear-gradient(rgba(0,212,255,.04) 1px,transparent 1px),
108
+ linear-gradient(90deg,rgba(0,212,255,.04) 1px,transparent 1px);
109
+ background-size:40px 40px;
110
+ pointer-events:none;
111
+ }
112
+
113
+ .wrapper{position:relative;z-index:1;max-width:1200px;margin:0 auto;padding:32px 24px 60px;}
114
+
115
+ /* ── Header ── */
116
+ header{display:flex;align-items:center;gap:16px;margin-bottom:36px;}
117
+ .logo-box{
118
+ width:52px;height:52px;border-radius:14px;
119
+ background:linear-gradient(135deg,var(--accent2),var(--accent));
120
+ display:flex;align-items:center;justify-content:center;font-size:22px;
121
+ box-shadow:var(--glow);
122
+ }
123
+ header h1{font-family:'Syne',sans-serif;font-size:26px;font-weight:800;letter-spacing:-.5px;}
124
+ header h1 span{color:var(--accent);}
125
+ .badge{
126
+ margin-left:auto;padding:5px 14px;border-radius:20px;font-size:11px;font-weight:700;
127
+ background:rgba(0,212,255,.1);border:1px solid rgba(0,212,255,.3);color:var(--accent);
128
+ letter-spacing:1.5px;text-transform:uppercase;
129
+ }
130
+
131
+ /* ── Difficulty row ── */
132
+ .diff-row{display:flex;gap:12px;margin-bottom:28px;flex-wrap:wrap;}
133
+ .diff-btn{
134
+ flex:1;min-width:120px;padding:13px 0;border-radius:12px;border:1.5px solid var(--border);
135
+ background:var(--card);cursor:pointer;font-family:'Syne',sans-serif;font-size:13px;font-weight:700;
136
+ color:var(--muted);letter-spacing:.8px;transition:all .2s;position:relative;overflow:hidden;
137
+ }
138
+ .diff-btn::after{
139
+ content:'';position:absolute;inset:0;opacity:0;
140
+ background:linear-gradient(135deg,rgba(0,212,255,.12),rgba(124,58,237,.12));
141
+ transition:opacity .2s;
142
+ }
143
+ .diff-btn:hover::after,.diff-btn.active::after{opacity:1;}
144
+ .diff-btn:hover,.diff-btn.active{border-color:var(--accent);color:var(--text);transform:translateY(-2px);box-shadow:var(--glow);}
145
+ .diff-btn.active{color:var(--accent);}
146
+ .diff-btn .pill{
147
+ display:inline-block;margin-left:8px;padding:2px 8px;border-radius:8px;font-size:10px;
148
+ vertical-align:middle;
149
+ }
150
+ .pill-easy{background:rgba(34,211,160,.15);color:var(--green);}
151
+ .pill-medium{background:rgba(251,191,36,.15);color:var(--yellow);}
152
+ .pill-hard{background:rgba(248,113,113,.15);color:var(--red);}
153
+ .pill-custom{background:rgba(124,58,237,.2);color:#a78bfa;}
154
+
155
+ /* ── Main grid ── */
156
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;}
157
+ @media(max-width:768px){.grid{grid-template-columns:1fr;}}
158
+
159
+ /* ── Card ── */
160
+ .card{
161
+ background:var(--card);border:1.5px solid var(--border);border-radius:16px;
162
+ padding:20px;
163
+ }
164
+ .card-title{
165
+ font-family:'Syne',sans-serif;font-size:11px;font-weight:800;letter-spacing:2px;
166
+ text-transform:uppercase;color:var(--muted);margin-bottom:14px;display:flex;align-items:center;gap:8px;
167
+ }
168
+ .card-title .dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);}
169
+
170
+ /* ── Textarea / pre ── */
171
+ textarea{
172
+ width:100%;height:150px;background:#060d1a;border:1.5px solid var(--border);
173
+ border-radius:10px;color:var(--text);font-family:'JetBrains Mono',monospace;
174
+ font-size:13px;padding:14px;resize:vertical;outline:none;transition:border-color .2s;
175
+ line-height:1.6;
176
+ }
177
+ textarea:focus{border-color:var(--accent);}
178
+ pre{
179
+ background:#060d1a;border:1.5px solid var(--border);border-radius:10px;
180
+ padding:14px;min-height:80px;font-size:13px;line-height:1.6;white-space:pre-wrap;
181
+ word-break:break-all;color:var(--green);overflow:auto;
182
+ }
183
+
184
+ /* ── Custom SQL ── */
185
+ .custom-area{display:none;margin-bottom:20px;}
186
+ .custom-area.show{display:block;}
187
+ .custom-area textarea{height:80px;}
188
+ .custom-label{font-size:11px;color:var(--muted);margin-bottom:6px;letter-spacing:1px;}
189
+
190
+ /* ── Run button ── */
191
+ .run-row{display:flex;gap:12px;margin-bottom:20px;align-items:center;}
192
+ .run-btn{
193
+ flex:1;padding:14px;border-radius:12px;border:none;cursor:pointer;
194
+ background:linear-gradient(135deg,var(--accent2),var(--accent));
195
+ font-family:'Syne',sans-serif;font-size:14px;font-weight:800;color:#fff;
196
+ letter-spacing:.5px;transition:all .2s;box-shadow:0 4px 24px rgba(0,212,255,.2);
197
+ }
198
+ .run-btn:hover{transform:translateY(-2px);box-shadow:0 8px 32px rgba(0,212,255,.35);}
199
+ .run-btn:active{transform:translateY(0);}
200
+
201
+ /* ── Score strip ── */
202
+ .score-strip{
203
+ display:flex;gap:14px;margin-bottom:20px;flex-wrap:wrap;
204
+ }
205
+ .score-card{
206
+ flex:1;min-width:110px;background:var(--card);border:1.5px solid var(--border);
207
+ border-radius:14px;padding:16px 14px;text-align:center;transition:all .3s;
208
+ }
209
+ .score-card.lit{border-color:var(--accent);box-shadow:var(--glow);}
210
+ .score-card .sc-val{font-family:'Syne',sans-serif;font-size:28px;font-weight:800;color:var(--accent);}
211
+ .score-card .sc-lbl{font-size:10px;color:var(--muted);margin-top:4px;letter-spacing:1.5px;text-transform:uppercase;}
212
+
213
+ /* ── Progress bar ── */
214
+ .prog-wrap{background:var(--surface);border-radius:99px;height:10px;overflow:hidden;margin-top:8px;}
215
+ .prog-bar{
216
+ height:100%;border-radius:99px;width:0%;
217
+ background:linear-gradient(90deg,var(--accent2),var(--accent));
218
+ transition:width .8s cubic-bezier(.4,0,.2,1);
219
+ box-shadow:0 0 12px var(--accent);
220
+ }
221
+
222
+ /* ── Status badge ── */
223
+ .status-pill{
224
+ display:inline-flex;align-items:center;gap:6px;padding:6px 14px;
225
+ border-radius:20px;font-size:12px;font-weight:700;letter-spacing:.5px;
226
+ margin-bottom:8px;
227
+ }
228
+ .status-pill.win{background:rgba(34,211,160,.15);border:1px solid var(--green);color:var(--green);}
229
+ .status-pill.lose{background:rgba(248,113,113,.12);border:1px solid var(--red);color:var(--red);}
230
+ .status-pill.neutral{background:rgba(91,112,146,.12);border:1px solid var(--muted);color:var(--muted);}
231
+
232
+ /* ── Chart ── */
233
+ .chart-wrap{position:relative;height:220px;}
234
+
235
+ /* ── Issues list ── */
236
+ .issues{list-style:none;}
237
+ .issues li{
238
+ padding:8px 12px;border-radius:8px;margin-bottom:6px;font-size:12px;
239
+ background:rgba(248,113,113,.08);border-left:3px solid var(--red);color:#fca5a5;
240
+ }
241
+
242
+ /* ── Log ── */
243
+ .log-wrap{
244
+ background:#060d1a;border:1.5px solid var(--border);border-radius:12px;
245
+ max-height:180px;overflow-y:auto;padding:12px;
246
+ }
247
+ .log-line{font-size:11px;line-height:1.8;color:var(--muted);}
248
+ .log-line span{color:var(--accent);}
249
+ .log-line.ok span{color:var(--green);}
250
+ .log-line.err span{color:var(--red);}
251
+
252
+ /* ── Explanation ── */
253
+ .explain-input{
254
+ width:100%;padding:10px 14px;border-radius:10px;border:1.5px solid var(--border);
255
+ background:#060d1a;color:var(--text);font-family:'JetBrains Mono',monospace;font-size:12px;
256
+ outline:none;transition:border-color .2s;
257
+ }
258
+ .explain-input:focus{border-color:var(--accent);}
259
+
260
+ /* ── Hackathon meter ── */
261
+ .hack-bar{
262
+ background:var(--card);border:1.5px solid var(--border);border-radius:16px;
263
+ padding:20px;margin-bottom:20px;
264
+ }
265
+ .hack-bar .hb-title{font-family:'Syne',sans-serif;font-size:12px;font-weight:800;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-bottom:12px;}
266
+ .hack-bar .hb-row{display:flex;justify-content:space-between;margin-bottom:8px;font-size:12px;}
267
+ .hack-bar .hb-val{color:var(--accent);font-weight:700;}
268
+ .hack-meter{position:relative;height:22px;background:var(--surface);border-radius:99px;overflow:hidden;}
269
+ .hack-fill{
270
+ height:100%;border-radius:99px;width:0%;
271
+ background:linear-gradient(90deg,#f43f5e,#fbbf24,#22d3a0,#00d4ff);
272
+ transition:width 1s cubic-bezier(.4,0,.2,1);
273
+ box-shadow:0 0 16px rgba(0,212,255,.4);
274
+ }
275
+ .hack-labels{display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:var(--muted);}
276
+
277
+ /* ── Animations ── */
278
+ @keyframes fadeUp{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:translateY(0);}}
279
+ .card,.score-card,.hack-bar{animation:fadeUp .4s ease both;}
280
+
281
+ /* ── Spinner ── */
282
+ @keyframes spin{to{transform:rotate(360deg);}}
283
+ .spinner{width:18px;height:18px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:none;}
284
+ .loading .spinner{display:block;}
285
+ .loading .btn-label{display:none;}
286
+ </style>
287
+ </head>
288
+ <body>
289
+ <div class="wrapper">
290
+
291
+ <!-- Header -->
292
+ <header>
293
+ <div class="logo-box">⚡</div>
294
+ <div>
295
+ <h1>SQL <span>Debugger</span> & Optimizer</h1>
296
+ <div style="font-size:11px;color:var(--muted);margin-top:2px;">RL Environment · OpenEnv Protocol</div>
297
+ </div>
298
+ <div class="badge">v1.0.0</div>
299
+ </header>
300
+
301
+ <!-- Difficulty Buttons -->
302
+ <div class="diff-row">
303
+ <button class="diff-btn active" data-task="easy" onclick="selectTask('easy',this)">
304
+ 🟢 EASY <span class="pill pill-easy">+100</span>
305
+ </button>
306
+ <button class="diff-btn" data-task="medium" onclick="selectTask('medium',this)">
307
+ 🟡 MEDIUM <span class="pill pill-medium">+200</span>
308
+ </button>
309
+ <button class="diff-btn" data-task="hard" onclick="selectTask('hard',this)">
310
+ 🔴 HARD <span class="pill pill-hard">+400</span>
311
+ </button>
312
+ <button class="diff-btn" data-task="custom" onclick="selectTask('custom',this)">
313
+ 🟣 CUSTOM <span class="pill pill-custom">+∞</span>
314
+ </button>
315
+ </div>
316
+
317
+ <!-- Custom SQL entry -->
318
+ <div class="custom-area" id="customArea">
319
+ <div class="custom-label">PASTE YOUR BROKEN SQL BELOW</div>
320
+ <textarea id="customSql" placeholder="-- Paste broken SQL here for custom challenge..."></textarea>
321
+ </div>
322
+
323
+ <!-- Explanation -->
324
+ <div style="margin-bottom:16px;">
325
+ <div class="custom-label" style="margin-bottom:6px;">FIX EXPLANATION (boosts score)</div>
326
+ <input class="explain-input" id="explanation" placeholder="e.g. Fixed typo in FROM clause, added missing JOIN condition..."/>
327
+ </div>
328
+
329
+ <!-- Run -->
330
+ <div class="run-row">
331
+ <button class="run-btn" id="runBtn" onclick="runFix()">
332
+ <div class="spinner" id="spinner"></div>
333
+ <span class="btn-label">⚡ RUN FIX &amp; SCORE</span>
334
+ </button>
335
+ </div>
336
+
337
+ <!-- Hackathon Meter -->
338
+ <div class="hack-bar">
339
+ <div class="hb-title">🏆 Hackathon Score Meter</div>
340
+ <div class="hb-row">
341
+ <span>Cumulative Score</span><span class="hb-val" id="hackScore">0</span>
342
+ </div>
343
+ <div class="hack-meter"><div class="hack-fill" id="hackFill"></div></div>
344
+ <div class="hack-labels">
345
+ <span>0</span><span>Participant</span><span>Good</span><span>Finalist</span><span>🥇 Winner</span>
346
+ </div>
347
+ </div>
348
+
349
+ <!-- Score Strip -->
350
+ <div class="score-strip">
351
+ <div class="score-card" id="scReward">
352
+ <div class="sc-val" id="valReward">—</div>
353
+ <div class="sc-lbl">Last Reward</div>
354
+ </div>
355
+ <div class="score-card" id="scTotal">
356
+ <div class="sc-val" id="valTotal">0</div>
357
+ <div class="sc-lbl">Total Score</div>
358
+ </div>
359
+ <div class="score-card" id="scRuns">
360
+ <div class="sc-val" id="valRuns">0</div>
361
+ <div class="sc-lbl">Runs</div>
362
+ </div>
363
+ <div class="score-card" id="scBest">
364
+ <div class="sc-val" id="valBest">—</div>
365
+ <div class="sc-lbl">Best Reward</div>
366
+ </div>
367
+ <div class="score-card" id="scAcc">
368
+ <div class="sc-val" id="valAcc">0%</div>
369
+ <div class="sc-lbl">Win Rate</div>
370
+ </div>
371
+ </div>
372
+
373
+ <!-- Main Grid -->
374
+ <div class="grid">
375
+ <!-- Left: SQL panels -->
376
+ <div>
377
+ <div class="card" style="margin-bottom:16px;">
378
+ <div class="card-title"><span class="dot"></span>BROKEN SQL (from challenge)</div>
379
+ <textarea id="sql" placeholder="Click a difficulty button above to load a challenge..."></textarea>
380
+ </div>
381
+ <div class="card">
382
+ <div class="card-title"><span class="dot" style="background:var(--green);box-shadow:0 0 8px var(--green);"></span>FIXED SQL OUTPUT</div>
383
+ <div id="statusPill"></div>
384
+ <pre id="out">-- Fixed SQL will appear here after running ⚡</pre>
385
+ <div style="margin-top:12px;">
386
+ <div class="custom-label" style="margin-bottom:4px;">SCORE PROGRESS</div>
387
+ <div class="prog-wrap"><div class="prog-bar" id="progBar"></div></div>
388
+ </div>
389
+ </div>
390
+ </div>
391
+
392
+ <!-- Right: Charts & logs -->
393
+ <div>
394
+ <div class="card" style="margin-bottom:16px;">
395
+ <div class="card-title"><span class="dot" style="background:var(--accent2);box-shadow:0 0 8px var(--accent2);"></span>REWARD HISTORY</div>
396
+ <div class="chart-wrap"><canvas id="rewardChart"></canvas></div>
397
+ </div>
398
+ <div class="card" style="margin-bottom:16px;">
399
+ <div class="card-title"><span class="dot" style="background:var(--yellow);box-shadow:0 0 8px var(--yellow);"></span>DIFFICULTY BREAKDOWN</div>
400
+ <div class="chart-wrap"><canvas id="diffChart"></canvas></div>
401
+ </div>
402
+ <div class="card">
403
+ <div class="card-title"><span class="dot"></span>SESSION LOG</div>
404
+ <div class="log-wrap" id="log">
405
+ <div class="log-line">Waiting for first run…</div>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <!-- Issues -->
412
+ <div class="card">
413
+ <div class="card-title"><span class="dot" style="background:var(--red);box-shadow:0 0 8px var(--red);"></span>DETECTED ISSUES</div>
414
+ <ul class="issues" id="issueList">
415
+ <li>No issues detected yet — run a fix to analyse SQL.</li>
416
+ </ul>
417
+ </div>
418
+
419
+ </div>
420
+
421
+ <script>
422
+ /* ─── State ─── */
423
+ let sid = null, challenge = null;
424
+ let currentTask = 'easy';
425
+ let totalScore = 0, runs = 0, wins = 0, bestReward = null;
426
+ const rewardHistory = [];
427
+ const diffScores = { easy:0, medium:0, hard:0, custom:0 };
428
+
429
+ const THRESHOLDS = { easy:0.6, medium:0.6, hard:0.6, custom:0.5 };
430
+
431
+ /* ─── Charts ─── */
432
+ const chartDefaults = {
433
+ color: '#e2eaf8',
434
+ plugins:{ legend:{ labels:{ color:'#5a7092', font:{ family:'JetBrains Mono', size:10 } } } },
435
+ scales:{
436
+ x:{ ticks:{ color:'#5a7092', font:{ family:'JetBrains Mono', size:10 } }, grid:{ color:'rgba(30,47,74,.6)' } },
437
+ y:{ ticks:{ color:'#5a7092', font:{ family:'JetBrains Mono', size:10 } }, grid:{ color:'rgba(30,47,74,.6)' } }
438
+ }
439
+ };
440
+
441
+ const rewardCtx = document.getElementById('rewardChart').getContext('2d');
442
+ const rewardChart = new Chart(rewardCtx, {
443
+ type: 'line',
444
+ data:{
445
+ labels:[],
446
+ datasets:[{
447
+ label:'Reward',
448
+ data:[],
449
+ borderColor:'#00d4ff',
450
+ backgroundColor:'rgba(0,212,255,.08)',
451
+ borderWidth:2,
452
+ pointBackgroundColor:'#00d4ff',
453
+ pointRadius:4,
454
+ tension:.4,
455
+ fill:true
456
+ }]
457
+ },
458
+ options:{ ...chartDefaults, animation:{ duration:600 }, plugins:{ legend:{ display:false } } }
459
+ });
460
+
461
+ const diffCtx = document.getElementById('diffChart').getContext('2d');
462
+ const diffChart = new Chart(diffCtx, {
463
+ type:'bar',
464
+ data:{
465
+ labels:['Easy','Medium','Hard','Custom'],
466
+ datasets:[{
467
+ label:'Score',
468
+ data:[0,0,0,0],
469
+ backgroundColor:['rgba(34,211,160,.7)','rgba(251,191,36,.7)','rgba(248,113,113,.7)','rgba(167,139,250,.7)'],
470
+ borderColor:['#22d3a0','#fbbf24','#f87171','#a78bfa'],
471
+ borderWidth:1.5,
472
+ borderRadius:6
473
+ }]
474
+ },
475
+ options:{ ...chartDefaults, animation:{ duration:600 }, plugins:{ legend:{ display:false } } }
476
+ });
477
+
478
+ /* ─── Select task ─── */
479
+ function selectTask(task, btn) {
480
+ currentTask = task;
481
+ document.querySelectorAll('.diff-btn').forEach(b=>b.classList.remove('active'));
482
+ btn.classList.add('active');
483
+
484
+ const ca = document.getElementById('customArea');
485
+ if(task==='custom') { ca.classList.add('show'); }
486
+ else { ca.classList.remove('show'); }
487
+
488
+ if(task !== 'custom') loadChallenge(task);
489
+ }
490
+
491
+ /* ─── Load challenge ─── */
492
+ async function loadChallenge(task) {
493
+ try {
494
+ const r = await fetch('/reset',{
495
+ method:'POST',
496
+ headers:{'Content-Type':'application/json'},
497
+ body: JSON.stringify({ task })
498
+ });
499
+ const d = await r.json();
500
+ sid = d.session_id;
501
+ challenge = d.observation.challenge;
502
+ document.getElementById('sql').value = challenge.broken_sql || '-- No SQL provided';
503
+ addLog('ok', `Challenge loaded · task=${task} · id=${challenge.id}`);
504
+ } catch(e) {
505
+ addLog('err', `Failed to load challenge: ${e.message}`);
506
+ }
507
+ }
508
+
509
+ /* ─── Run Fix ─── */
510
+ async function runFix() {
511
+ const btn = document.getElementById('runBtn');
512
+ btn.classList.add('loading');
513
+
514
+ try {
515
+ let brokenSql = document.getElementById('sql').value.trim();
516
+ let customSql = document.getElementById('customSql').value.trim();
517
+ const explanation = document.getElementById('explanation').value.trim() || 'auto fix';
518
+
519
+ // For custom, auto-load a session first
520
+ if(currentTask === 'custom') {
521
+ if(!customSql) { addLog('err','Paste your broken SQL in the custom box.'); btn.classList.remove('loading'); return; }
522
+ // Load a session for custom (use easy as base env)
523
+ const rr = await fetch('/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({task:'easy'})});
524
+ const rd = await rr.json();
525
+ sid = rd.session_id;
526
+ challenge = rd.observation.challenge;
527
+ brokenSql = customSql;
528
+ challenge.broken_sql = customSql;
529
+ }
530
+
531
+ if(!sid || !challenge) { addLog('err','Click a difficulty button first.'); btn.classList.remove('loading'); return; }
532
+
533
+ /* ── Smart fixer ── */
534
+ const fixed = smartFix(brokenSql);
535
+ const issues = detectIssues(brokenSql);
536
+
537
+ const resp = await fetch('/step',{
538
+ method:'POST',
539
+ headers:{'Content-Type':'application/json'},
540
+ body: JSON.stringify({
541
+ session_id: sid,
542
+ action:{
543
+ challenge_id: challenge.id,
544
+ fixed_sql: fixed,
545
+ explanation: explanation,
546
+ detected_issues: issues
547
+ }
548
+ })
549
+ });
550
+ const data = await resp.json();
551
+ const reward = data.reward ?? 0;
552
+
553
+ /* ── Update UI ── */
554
+ updateScores(reward, issues, fixed, data);
555
+
556
+ /* ── Reload next challenge ── */
557
+ if(currentTask !== 'custom') loadChallenge(currentTask);
558
+
559
+ } catch(e) {
560
+ addLog('err', `Error: ${e.message}`);
561
+ } finally {
562
+ btn.classList.remove('loading');
563
+ }
564
+ }
565
+
566
+ /* ─── Smart SQL fixer (high-reward logic) ─── */
567
+ function smartFix(sql) {
568
+ let s = sql;
569
+
570
+ // Keyword typos
571
+ const fixes = [
572
+ [/\bSELCT\b/gi,'SELECT'],[/\bSELECT\b/gi,'SELECT'],
573
+ [/\bFORM\b/g,'FROM'],[/\bFROM\b/gi,'FROM'],
574
+ [/\bWHER\b/g,'WHERE'],[/\bWHERE\b/gi,'WHERE'],
575
+ [/\bORDER\s+BU\b/gi,'ORDER BY'],[/\bGROUP\s+BU\b/gi,'GROUP BY'],
576
+ [/\bHAVNG\b/gi,'HAVING'],[/\bHAVING\b/gi,'HAVING'],
577
+ [/\bINNER\s+JION\b/gi,'INNER JOIN'],[/\bLEFT\s+JION\b/gi,'LEFT JOIN'],
578
+ [/\bJOIN\s+ON\b/gi,'JOIN'],[/\bINSERT\s+IN\b/g,'INSERT INTO'],
579
+ [/\bDELETE\s+FORM\b/gi,'DELETE FROM'],
580
+ [/\bUPDATE\b/gi,'UPDATE'],[/\bSET\b/gi,'SET'],
581
+ [/\bDISTINT\b/gi,'DISTINCT'],[/\bCOUNT\s*\(\s*\)/gi,'COUNT(*)'],
582
+ [/\bNULL\b/gi,'NULL'],[/\bIS\s+NOT\s+NUL\b/gi,'IS NOT NULL'],
583
+ [/\bIS\s+NUL\b/gi,'IS NULL'],
584
+ [/\bLIMT\b/gi,'LIMIT'],[/\bOFFST\b/gi,'OFFSET'],
585
+ [/\bUNION\s+AL\b/gi,'UNION ALL'],
586
+ [/\bCREATE\s+TABL\b/gi,'CREATE TABLE'],
587
+ [/\bALTER\s+TABL\b/gi,'ALTER TABLE'],
588
+ [/\bDROP\s+TABL\b/gi,'DROP TABLE'],
589
+ [/\bVARCAHR\b/gi,'VARCHAR'],[/\bINTEGR\b/gi,'INTEGER'],
590
+ [/==\s*/g,'= '],[/\bAND\s+AND\b/gi,'AND'],
591
+ [/\bOR\s+OR\b/gi,'OR'],[/\bNOT\s+NOT\b/gi,'NOT'],
592
+ [/\bTRUNCATE\b/gi,'TRUNCATE'],[/\bTRANSACTION\b/gi,'TRANSACTION'],
593
+ ];
594
+
595
+ fixes.forEach(([pat,rep])=>{ s = s.replace(pat,rep); });
596
+
597
+ // Unclosed quotes fix
598
+ const sq = (s.match(/'/g)||[]).length;
599
+ if(sq%2!==0) s += "'";
600
+
601
+ // Unclosed parens fix
602
+ const op=(s.match(/\(/g)||[]).length, cl=(s.match(/\)/g)||[]).length;
603
+ if(op>cl) s += ')'.repeat(op-cl);
604
+ if(cl>op) s = '('.repeat(cl-op) + s;
605
+
606
+ // Missing semicolon
607
+ if(!/;\s*$/.test(s.trim())) s = s.trim() + ';';
608
+
609
+ // Normalise whitespace
610
+ s = s.replace(/\s{2,}/g,' ').trim();
611
+
612
+ return s;
613
+ }
614
+
615
+ /* ─── Detect issues ─── */
616
+ function detectIssues(sql) {
617
+ const issues = [];
618
+ if(/\bSELCT\b/i.test(sql)) issues.push('typo:SELCT→SELECT');
619
+ if(/\bFORM\b/g.test(sql)) issues.push('typo:FORM→FROM');
620
+ if(/\bWHER\b/g.test(sql)) issues.push('typo:WHER→WHERE');
621
+ if(/==/.test(sql)) issues.push('operator:==→=');
622
+ if((/\(/g.exec(sql)||[]).length !== (/\)/g.exec(sql)||[]).length) issues.push('syntax:unbalanced_parentheses');
623
+ if((/'/g.exec(sql)||[]).length%2!==0) issues.push('syntax:unclosed_string_literal');
624
+ if(!/;\s*$/.test(sql.trim())) issues.push('syntax:missing_semicolon');
625
+ if(/\bLEFT\s+JION\b/i.test(sql)||/\bINNER\s+JION\b/i.test(sql)) issues.push('typo:JION→JOIN');
626
+ if(/\bHAVNG\b/i.test(sql)) issues.push('typo:HAVNG→HAVING');
627
+ if(/\bINTEGR\b/i.test(sql)) issues.push('typo:INTEGR→INTEGER');
628
+ if(!issues.length) issues.push('no_issues_detected');
629
+ return issues;
630
+ }
631
+
632
+ /* ─── Update all UI ─── */
633
+ function updateScores(reward, issues, fixed, data) {
634
+ runs++;
635
+ totalScore += reward;
636
+ const isWin = reward >= (THRESHOLDS[currentTask] || 0.6);
637
+ if(isWin) wins++;
638
+ if(bestReward===null || reward > bestReward) bestReward = reward;
639
+
640
+ diffScores[currentTask] += reward;
641
+
642
+ /* Score cards */
643
+ document.getElementById('valReward').textContent = reward.toFixed ? reward.toFixed(3) : reward;
644
+ document.getElementById('valTotal').textContent = totalScore.toFixed(2);
645
+ document.getElementById('valRuns').textContent = runs;
646
+ document.getElementById('valBest').textContent = bestReward.toFixed ? bestReward.toFixed(3) : bestReward;
647
+ document.getElementById('valAcc').textContent = Math.round((wins/runs)*100)+'%';
648
+
649
+ document.getElementById('scReward').classList.add('lit');
650
+ setTimeout(()=>document.getElementById('scReward').classList.remove('lit'),1200);
651
+
652
+ /* Progress bar */
653
+ const pct = Math.min(100, (reward/(THRESHOLDS[currentTask]||1))*100);
654
+ document.getElementById('progBar').style.width = pct+'%';
655
+
656
+ /* Hackathon meter */
657
+ const hackPct = Math.min(100, (totalScore / (runs * 1)) * 100);
658
+ document.getElementById('hackFill').style.width = hackPct+'%';
659
+ document.getElementById('hackScore').textContent = totalScore.toFixed(2);
660
+
661
+ /* Status pill */
662
+ const pill = document.getElementById('statusPill');
663
+ if(isWin){
664
+ pill.innerHTML = '<div class="status-pill win">✅ WINNING SCORE ACHIEVED</div>';
665
+ } else {
666
+ pill.innerHTML = '<div class="status-pill lose">⚠ BELOW THRESHOLD — TRY AGAIN</div>';
667
+ }
668
+
669
+ /* Output */
670
+ document.getElementById('out').textContent = fixed;
671
+
672
+ /* Reward chart */
673
+ rewardHistory.push(reward);
674
+ rewardChart.data.labels.push(`Run ${runs}`);
675
+ rewardChart.data.datasets[0].data.push(reward);
676
+ if(rewardHistory.length > 20) {
677
+ rewardChart.data.labels.shift();
678
+ rewardChart.data.datasets[0].data.shift();
679
+ }
680
+ rewardChart.update();
681
+
682
+ /* Diff chart */
683
+ diffChart.data.datasets[0].data = [
684
+ diffScores.easy, diffScores.medium, diffScores.hard, diffScores.custom
685
+ ];
686
+ diffChart.update();
687
+
688
+ /* Issues list */
689
+ const ul = document.getElementById('issueList');
690
+ ul.innerHTML = issues.map(i=>`<li>${i.replace(/:/g,' → ')}</li>`).join('');
691
+
692
+ /* Log */
693
+ addLog(isWin?'ok':'err',
694
+ `run=${runs} task=${currentTask} reward=${typeof reward==='number'?reward.toFixed(3):reward} done=${data.done||false}`);
695
+ }
696
+
697
+ /* ─── Log helper ─── */
698
+ function addLog(type, msg) {
699
+ const box = document.getElementById('log');
700
+ const ts = new Date().toLocaleTimeString();
701
+ const div = document.createElement('div');
702
+ div.className = `log-line ${type}`;
703
+ div.innerHTML = `<span>[${ts}]</span> ${msg}`;
704
+ box.appendChild(div);
705
+ box.scrollTop = box.scrollHeight;
706
+ // Clear placeholder
707
+ const first = box.querySelector('.log-line:not(.ok):not(.err)');
708
+ if(first && box.children.length > 1) first.remove();
709
+ }
710
+
711
+ /* ─── Initial load ─── */
712
+ loadChallenge('easy');
713
+ </script>
714
+ </body>
715
+ </html>"""
716
+
717
+
718
+ def main():
719
+ import uvicorn
720
+ uvicorn.run("server.app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
721
+
722
+
723
+ if __name__ == "__main__":
724
+ main()
sql_debugger_env.py ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SQL Query Debugger & Optimizer — OpenEnv Environment
3
+ ======================================================
4
+ A real-world environment where an AI agent is given broken, inefficient,
5
+ or insecure SQL queries and must:
6
+ - Fix syntax errors
7
+ - Correct logical bugs (wrong JOIN, missing GROUP BY, etc.)
8
+ - Optimize for performance (remove N+1 patterns, add proper indexes hints)
9
+ - Detect & fix SQL injection vulnerabilities
10
+
11
+ This is a task real data analysts and backend engineers do every single day.
12
+
13
+ Tasks:
14
+ easy — Fix obvious syntax errors in simple SELECT queries
15
+ medium — Fix logic bugs (wrong JOINs, incorrect aggregations, missing clauses)
16
+ hard — Fix + optimize + secure (injection detection, N+1 queries, subquery rewrites)
17
+
18
+ Why this beats other environments:
19
+ - 100% deterministic graders (execute real SQL in SQLite, compare results)
20
+ - Rich partial rewards at every step (syntax → logic → performance → security)
21
+ - Real engineering pain point that Meta/HF engineers deal with daily
22
+ - Novel domain — not in OpenEnv Hub yet
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ import sqlite3
29
+ import textwrap
30
+ from typing import Any, Dict, List, Literal, Optional, Tuple
31
+
32
+ from pydantic import BaseModel, Field
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Database Schema (in-memory SQLite — fully reproducible)
36
+ # ---------------------------------------------------------------------------
37
+
38
+ SCHEMA_SQL = """
39
+ CREATE TABLE IF NOT EXISTS users (
40
+ id INTEGER PRIMARY KEY,
41
+ name TEXT NOT NULL,
42
+ email TEXT UNIQUE NOT NULL,
43
+ department TEXT NOT NULL,
44
+ salary REAL NOT NULL,
45
+ hire_date TEXT NOT NULL,
46
+ manager_id INTEGER REFERENCES users(id)
47
+ );
48
+
49
+ CREATE TABLE IF NOT EXISTS orders (
50
+ id INTEGER PRIMARY KEY,
51
+ user_id INTEGER NOT NULL REFERENCES users(id),
52
+ product TEXT NOT NULL,
53
+ amount REAL NOT NULL,
54
+ status TEXT NOT NULL CHECK(status IN ('pending','completed','cancelled')),
55
+ created_at TEXT NOT NULL
56
+ );
57
+
58
+ CREATE TABLE IF NOT EXISTS products (
59
+ id INTEGER PRIMARY KEY,
60
+ name TEXT NOT NULL,
61
+ category TEXT NOT NULL,
62
+ price REAL NOT NULL,
63
+ stock INTEGER NOT NULL DEFAULT 0
64
+ );
65
+ """
66
+
67
+ SEED_SQL = """
68
+ INSERT INTO users VALUES
69
+ (1,'Alice Chen','alice@co.com','Engineering',95000,'2021-03-01',NULL),
70
+ (2,'Bob Smith','bob@co.com','Engineering',82000,'2022-06-15',1),
71
+ (3,'Carol Jones','carol@co.com','Marketing',74000,'2020-01-10',NULL),
72
+ (4,'Dan Park','dan@co.com','Engineering',91000,'2019-08-22',1),
73
+ (5,'Eva Liu','eva@co.com','Marketing',68000,'2023-02-28',3),
74
+ (6,'Frank Wu','frank@co.com','HR',61000,'2021-11-05',NULL),
75
+ (7,'Grace Kim','grace@co.com','Engineering',103000,'2018-05-14',1),
76
+ (8,'Hank Patel','hank@co.com','Marketing',72000,'2022-09-30',3);
77
+
78
+ INSERT INTO products VALUES
79
+ (1,'Laptop Pro','Electronics',1299.99,45),
80
+ (2,'Wireless Mouse','Electronics',29.99,200),
81
+ (3,'Standing Desk','Furniture',549.00,30),
82
+ (4,'Monitor 4K','Electronics',699.99,60),
83
+ (5,'Ergonomic Chair','Furniture',399.00,25),
84
+ (6,'USB Hub','Electronics',49.99,150);
85
+
86
+ INSERT INTO orders VALUES
87
+ (1,1,'Laptop Pro',1299.99,'completed','2024-01-15'),
88
+ (2,2,'Wireless Mouse',29.99,'completed','2024-01-20'),
89
+ (3,1,'Monitor 4K',699.99,'completed','2024-02-01'),
90
+ (4,3,'Ergonomic Chair',399.00,'pending','2024-02-10'),
91
+ (5,4,'Standing Desk',549.00,'completed','2024-02-14'),
92
+ (6,2,'USB Hub',49.99,'cancelled','2024-02-20'),
93
+ (7,5,'Wireless Mouse',29.99,'completed','2024-03-01'),
94
+ (8,7,'Laptop Pro',1299.99,'completed','2024-03-05'),
95
+ (9,1,'USB Hub',49.99,'completed','2024-03-10'),
96
+ (10,3,'Laptop Pro',1299.99,'pending','2024-03-15'),
97
+ (11,4,'Wireless Mouse',29.99,'completed','2024-03-18'),
98
+ (12,6,'Ergonomic Chair',399.00,'completed','2024-03-20');
99
+ """
100
+
101
+ def make_db() -> sqlite3.Connection:
102
+ """Create fresh in-memory SQLite database."""
103
+ conn = sqlite3.connect(":memory:")
104
+ conn.row_factory = sqlite3.Row
105
+ conn.executescript(SCHEMA_SQL)
106
+ conn.executescript(SEED_SQL)
107
+ conn.commit()
108
+ return conn
109
+
110
+ def run_query(conn: sqlite3.Connection, sql: str) -> Tuple[List[Dict], Optional[str]]:
111
+ """Execute SQL, return (rows, error). rows=[] on error."""
112
+ try:
113
+ cur = conn.execute(sql)
114
+ rows = [dict(r) for r in cur.fetchall()]
115
+ return rows, None
116
+ except Exception as e:
117
+ return [], str(e)
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Pydantic Models
121
+ # ---------------------------------------------------------------------------
122
+
123
+ class SQLChallenge(BaseModel):
124
+ id: str
125
+ description: str # What the query SHOULD do (plain English)
126
+ broken_sql: str # The buggy SQL given to the agent
127
+ expected_row_count: int # How many rows the correct query returns
128
+ expected_columns: List[str] # Column names of correct output
129
+ hint: str # Nudge without giving the answer
130
+ difficulty: str
131
+ bugs: List[str] # Human-readable bug descriptions (shown to agent)
132
+
133
+
134
+ class Observation(BaseModel):
135
+ challenge: SQLChallenge
136
+ schema_info: str # DDL so agent knows table structure
137
+ current_step: int
138
+ max_steps: int
139
+ task: str
140
+ previous_attempts: List[Dict[str, Any]] = Field(default_factory=list)
141
+ instructions: str
142
+
143
+
144
+ class Action(BaseModel):
145
+ challenge_id: str
146
+ fixed_sql: str # The agent's corrected SQL
147
+ explanation: Optional[str] = None # Why the original was wrong
148
+ detected_issues: List[str] = Field(default_factory=list) # e.g. ["missing_group_by","sql_injection"]
149
+
150
+
151
+ class Reward(BaseModel):
152
+ value: float
153
+ breakdown: Dict[str, float]
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Challenges Dataset
158
+ # ---------------------------------------------------------------------------
159
+
160
+ CHALLENGES: List[Dict] = [
161
+
162
+ # =========== EASY ===========
163
+ {
164
+ "id": "sq001",
165
+ "difficulty": "easy",
166
+ "description": "Get the names and salaries of all employees in the Engineering department, ordered by salary descending.",
167
+ "broken_sql": """
168
+ SELCT name, salary FORM users
169
+ WHER department = 'Engineering'
170
+ ORDER BY salary DESC
171
+ """.strip(),
172
+ "expected_row_count": 4,
173
+ "expected_columns": ["name", "salary"],
174
+ "hint": "Look carefully at the SQL keywords — any typos?",
175
+ "bugs": [
176
+ "Typo: SELCT should be SELECT",
177
+ "Typo: FORM should be FROM",
178
+ "Typo: WHER should be WHERE",
179
+ ],
180
+ },
181
+ {
182
+ "id": "sq002",
183
+ "difficulty": "easy",
184
+ "description": "Count how many orders each user has made, showing user_id and their order count.",
185
+ "broken_sql": """
186
+ SELECT user_id, COUNT(*) as order_count
187
+ FROM orders
188
+ """.strip(),
189
+ "expected_row_count": 7,
190
+ "expected_columns": ["user_id", "order_count"],
191
+ "hint": "When using COUNT with a non-aggregate column, something is missing.",
192
+ "bugs": ["Missing GROUP BY user_id — without it, SQLite returns only 1 row instead of per-user counts"],
193
+ },
194
+ {
195
+ "id": "sq003",
196
+ "difficulty": "easy",
197
+ "description": "Get the total revenue from all completed orders.",
198
+ "broken_sql": """
199
+ SELECT SUM(amount) AS total_revenue
200
+ FROM orders
201
+ WHERE status = 'complete'
202
+ """.strip(),
203
+ "expected_row_count": 1,
204
+ "expected_columns": ["total_revenue"],
205
+ "hint": "Check the exact value used in the WHERE condition against the schema.",
206
+ "bugs": ["Wrong status value: 'complete' should be 'completed' (as defined in the CHECK constraint)"],
207
+ },
208
+
209
+ # =========== MEDIUM ===========
210
+ {
211
+ "id": "sq004",
212
+ "difficulty": "medium",
213
+ "description": "Get each user's name and the total amount they've spent on completed orders. Include users who have no completed orders (show 0 for them).",
214
+ "broken_sql": """
215
+ SELECT u.name, SUM(o.amount) AS total_spent
216
+ FROM orders o
217
+ INNER JOIN users u ON u.id = o.user_id
218
+ WHERE o.status = 'completed'
219
+ GROUP BY u.name
220
+ """.strip(),
221
+ "expected_row_count": 8,
222
+ "expected_columns": ["name", "total_spent"],
223
+ "hint": "The problem statement says 'Include users who have no completed orders' — does INNER JOIN do that?",
224
+ "bugs": [
225
+ "INNER JOIN excludes users with no completed orders — should be LEFT JOIN from users to orders",
226
+ "The WHERE clause further removes non-matching rows — it should move to an ON clause or be handled with COALESCE",
227
+ ],
228
+ },
229
+ {
230
+ "id": "sq005",
231
+ "difficulty": "medium",
232
+ "description": "Find all Engineering employees who earn more than the average salary of their own department.",
233
+ "broken_sql": """
234
+ SELECT name, salary
235
+ FROM users
236
+ WHERE department = 'Engineering'
237
+ AND salary > (SELECT AVG(salary) FROM users)
238
+ """.strip(),
239
+ "expected_row_count": 2,
240
+ "expected_columns": ["name", "salary"],
241
+ "hint": "The subquery calculates something — but is it the average of the right group?",
242
+ "bugs": [
243
+ "Subquery uses AVG(salary) across ALL departments, not just Engineering",
244
+ "Fix: WHERE department = 'Engineering' AND salary > (SELECT AVG(salary) FROM users WHERE department = 'Engineering')",
245
+ ],
246
+ },
247
+ {
248
+ "id": "sq006",
249
+ "difficulty": "medium",
250
+ "description": "Get the top 3 products by total revenue from completed orders.",
251
+ "broken_sql": """
252
+ SELECT product, SUM(amount) AS revenue
253
+ FROM orders
254
+ WHERE status = 'completed'
255
+ GROUP BY product
256
+ ORDER BY amount DESC
257
+ LIMIT 3
258
+ """.strip(),
259
+ "expected_row_count": 3,
260
+ "expected_columns": ["product", "revenue"],
261
+ "hint": "The ORDER BY is using a column — but which column should you order by to rank by total revenue?",
262
+ "bugs": [
263
+ "ORDER BY amount DESC orders by the raw column, not the aggregated revenue",
264
+ "Fix: ORDER BY revenue DESC (or ORDER BY SUM(amount) DESC)",
265
+ ],
266
+ },
267
+
268
+ # =========== HARD ===========
269
+ {
270
+ "id": "sq007",
271
+ "difficulty": "hard",
272
+ "description": "For each department, show the department name, number of employees, average salary, and highest salary. Only include departments with more than 1 employee.",
273
+ "broken_sql": """
274
+ SELECT department,
275
+ COUNT(id),
276
+ AVG(salary) AS avg_salary,
277
+ MAX(salary)
278
+ FROM users
279
+ GROUP BY department
280
+ HAVING COUNT(*) > 1
281
+ ORDER BY avg_salary
282
+ """.strip(),
283
+ "expected_row_count": 2,
284
+ "expected_columns": ["department", "COUNT(id)", "avg_salary", "MAX(salary)"],
285
+ "hint": "The query runs but has style/clarity issues AND a subtle ordering issue. Also check column aliases.",
286
+ "bugs": [
287
+ "COUNT(id) and MAX(salary) have no aliases — hard to read, unpredictable column names in results",
288
+ "ORDER BY avg_salary is ascending by default — descending is typically more useful for salary reports",
289
+ "Minor: COUNT(id) should be COUNT(*) or COUNT(1) for clarity",
290
+ ],
291
+ },
292
+ {
293
+ "id": "sq008",
294
+ "difficulty": "hard",
295
+ "description": "SECURITY: A web app builds this query using user input for `dept`. Find and fix the SQL injection vulnerability. The query should return employee names for a given department.",
296
+ "broken_sql": """
297
+ SELECT name FROM users WHERE department = '\" + dept + \"'
298
+ """.strip(),
299
+ "expected_row_count": 4,
300
+ "expected_columns": ["name"],
301
+ "hint": "String concatenation of user input into SQL = SQL injection. The fix is parameterized queries.",
302
+ "bugs": [
303
+ "SQL injection vulnerability: user input `dept` is directly concatenated into the query string",
304
+ "Fix: Use parameterized query: SELECT name FROM users WHERE department = ? with parameters=(dept,)",
305
+ "For this environment, rewrite as: SELECT name FROM users WHERE department = 'Engineering'",
306
+ ],
307
+ },
308
+ {
309
+ "id": "sq009",
310
+ "difficulty": "hard",
311
+ "description": "Rewrite this inefficient N+1 style subquery pattern into a single efficient JOIN query. Get user names and their most recent order's product name and amount.",
312
+ "broken_sql": """
313
+ SELECT
314
+ u.name,
315
+ (SELECT product FROM orders WHERE user_id = u.id ORDER BY created_at DESC LIMIT 1) AS last_product,
316
+ (SELECT amount FROM orders WHERE user_id = u.id ORDER BY created_at DESC LIMIT 1) AS last_amount
317
+ FROM users u
318
+ """.strip(),
319
+ "expected_row_count": 8,
320
+ "expected_columns": ["name", "last_product", "last_amount"],
321
+ "hint": "Two correlated subqueries hit the orders table twice per user. Use a CTE or subquery with ROW_NUMBER() or a ranked join instead.",
322
+ "bugs": [
323
+ "N+1 query pattern: two correlated subqueries each scan orders once per user = O(n) full scans",
324
+ "Fix: Use a single subquery that gets the latest order per user, then LEFT JOIN it",
325
+ ],
326
+ },
327
+ ]
328
+
329
+ CHALLENGE_MAP: Dict[str, Dict] = {c["id"]: c for c in CHALLENGES}
330
+
331
+ # Correct reference SQL for each challenge (used internally for grading)
332
+ REFERENCE_SQL: Dict[str, str] = {
333
+ "sq001": "SELECT name, salary FROM users WHERE department = 'Engineering' ORDER BY salary DESC",
334
+ "sq002": "SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id",
335
+ "sq003": "SELECT SUM(amount) AS total_revenue FROM orders WHERE status = 'completed'",
336
+ "sq004": """
337
+ SELECT u.name, COALESCE(SUM(o.amount), 0) AS total_spent
338
+ FROM users u
339
+ LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'completed'
340
+ GROUP BY u.id, u.name
341
+ """,
342
+ "sq005": """
343
+ SELECT name, salary FROM users
344
+ WHERE department = 'Engineering'
345
+ AND salary > (SELECT AVG(salary) FROM users WHERE department = 'Engineering')
346
+ """,
347
+ "sq006": """
348
+ SELECT product, SUM(amount) AS revenue
349
+ FROM orders WHERE status = 'completed'
350
+ GROUP BY product ORDER BY revenue DESC LIMIT 3
351
+ """,
352
+ "sq007": """
353
+ SELECT department, COUNT(*) AS employee_count, AVG(salary) AS avg_salary, MAX(salary) AS max_salary
354
+ FROM users GROUP BY department HAVING COUNT(*) > 1 ORDER BY avg_salary DESC
355
+ """,
356
+ "sq008": "SELECT name FROM users WHERE department = 'Engineering'",
357
+ "sq009": """
358
+ SELECT u.name, latest.product AS last_product, latest.amount AS last_amount
359
+ FROM users u
360
+ LEFT JOIN (
361
+ SELECT user_id, product, amount,
362
+ ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
363
+ FROM orders
364
+ ) latest ON latest.user_id = u.id AND latest.rn = 1
365
+ """,
366
+ }
367
+
368
+ # =========== Task Config ===========
369
+ TASK_CONFIGS = {
370
+ "easy": {"challenge_ids": ["sq001", "sq002", "sq003"], "max_steps": 3},
371
+ "medium": {"challenge_ids": ["sq004", "sq005", "sq006"], "max_steps": 3},
372
+ "hard": {"challenge_ids": ["sq007", "sq008", "sq009"], "max_steps": 3},
373
+ }
374
+
375
+ SCHEMA_INFO = textwrap.dedent("""
376
+ TABLE users(id, name, email, department, salary, hire_date, manager_id)
377
+ - department: 'Engineering' | 'Marketing' | 'HR'
378
+ - salary: REAL
379
+ - manager_id: FK → users.id (nullable)
380
+
381
+ TABLE orders(id, user_id, product, amount, status, created_at)
382
+ - status: 'pending' | 'completed' | 'cancelled'
383
+ - user_id: FK → users.id
384
+
385
+ TABLE products(id, name, category, price, stock)
386
+ - category: 'Electronics' | 'Furniture'
387
+ """).strip()
388
+
389
+ INSTRUCTIONS = {
390
+ "easy": (
391
+ "EASY TASK — Fix SQL Syntax Errors\n"
392
+ "Each challenge gives you a broken SQL query with typos or missing keywords.\n"
393
+ "Fix the SQL so it runs correctly and returns the expected results.\n"
394
+ "Set fixed_sql to your corrected query."
395
+ ),
396
+ "medium": (
397
+ "MEDIUM TASK — Fix Logic Bugs\n"
398
+ "Each query runs without error but produces WRONG results due to logic bugs:\n"
399
+ "wrong JOIN type, wrong column in ORDER BY, missing GROUP BY, wrong subquery scope.\n"
400
+ "Fix the SQL to return the correct, expected result set."
401
+ ),
402
+ "hard": (
403
+ "HARD TASK — Fix, Optimize & Secure\n"
404
+ "Each query has logic bugs AND performance/security issues:\n"
405
+ "SQL injection vulnerabilities, N+1 correlated subqueries, missing aliases.\n"
406
+ "Fix ALL issues. For SQL injection: rewrite using safe parameterized form.\n"
407
+ "For N+1 queries: rewrite as a single efficient JOIN. List all detected_issues."
408
+ ),
409
+ }
410
+
411
+
412
+ # ---------------------------------------------------------------------------
413
+ # Environment
414
+ # ---------------------------------------------------------------------------
415
+
416
+ class SQLDebuggerEnv:
417
+ """OpenEnv-compliant SQL Debugger & Optimizer environment."""
418
+
419
+ def __init__(self, task: str = "easy"):
420
+ if task not in TASK_CONFIGS:
421
+ raise ValueError(f"Unknown task '{task}'. Choose: {list(TASK_CONFIGS)}")
422
+ self.task = task
423
+ self._cfg = TASK_CONFIGS[task]
424
+ self._challenges: List[SQLChallenge] = []
425
+ self._step = 0
426
+ self._done = False
427
+ self._history: List[Dict] = []
428
+ self._results: Dict[str, Dict] = {}
429
+ self._db: Optional[sqlite3.Connection] = None
430
+ self.reset()
431
+
432
+ # ------------------------------------------------------------------
433
+ # OpenEnv interface
434
+ # ------------------------------------------------------------------
435
+
436
+ def reset(self) -> Observation:
437
+ self._db = make_db()
438
+ self._challenges = [
439
+ SQLChallenge(**CHALLENGE_MAP[cid])
440
+ for cid in self._cfg["challenge_ids"]
441
+ ]
442
+ self._step = 0
443
+ self._done = False
444
+ self._history = []
445
+ self._results = {}
446
+ return self._make_observation(0)
447
+
448
+ def step(self, action: Action) -> Tuple[Observation, float, bool, Dict]:
449
+ if self._done:
450
+ raise RuntimeError("Episode done. Call reset().")
451
+
452
+ self._step += 1
453
+ challenge = self._find_challenge(action.challenge_id)
454
+ idx = self._challenge_index(action.challenge_id)
455
+
456
+ if challenge is None or idx is None:
457
+ reward = -0.1
458
+ info = {"error": "invalid challenge_id", "breakdown": {}}
459
+ else:
460
+ breakdown = self._grade(challenge, action)
461
+ reward = max(0.0, min(1.0, sum(breakdown.values())))
462
+ info = {"breakdown": breakdown, "step": self._step}
463
+ self._results[action.challenge_id] = {
464
+ "action": action.model_dump(),
465
+ "reward": reward,
466
+ "breakdown": breakdown,
467
+ }
468
+
469
+ self._history.append({
470
+ "step": self._step,
471
+ "challenge_id": action.challenge_id,
472
+ "reward": reward,
473
+ "info": info,
474
+ })
475
+
476
+ next_idx = min(idx + 1 if idx is not None else 0, len(self._challenges) - 1)
477
+ self._done = (
478
+ self._step >= self._cfg["max_steps"]
479
+ or len(self._results) >= len(self._challenges)
480
+ )
481
+
482
+ obs = self._make_observation(next_idx if not self._done else idx)
483
+ return obs, round(reward, 4), self._done, info
484
+
485
+ def state(self) -> Dict[str, Any]:
486
+ return {
487
+ "task": self.task,
488
+ "step": self._step,
489
+ "done": self._done,
490
+ "challenges_total": len(self._challenges),
491
+ "challenges_solved": len(self._results),
492
+ "history": self._history,
493
+ }
494
+
495
+ def close(self):
496
+ if self._db:
497
+ self._db.close()
498
+
499
+ def episode_score(self) -> float:
500
+ if not self._results:
501
+ return 0.0
502
+ total = sum(r["reward"] for r in self._results.values())
503
+ return round(total / len(self._challenges), 4)
504
+
505
+ # ------------------------------------------------------------------
506
+ # Grading (deterministic — runs real SQL)
507
+ # ------------------------------------------------------------------
508
+
509
+ def _grade(self, challenge: SQLChallenge, action: Action) -> Dict[str, float]:
510
+ bd: Dict[str, float] = {}
511
+ fixed_sql = action.fixed_sql.strip()
512
+
513
+ # 1. Syntax check — does the fixed SQL run at all?
514
+ rows, error = run_query(self._db, fixed_sql)
515
+ if error:
516
+ bd["syntax_error"] = 0.0
517
+ bd["parse_penalty"] = -0.05
518
+ return bd
519
+ bd["syntax_ok"] = 0.20
520
+
521
+ # 2. Row count match
522
+ ref_rows, _ = run_query(self._db, REFERENCE_SQL[challenge.id])
523
+ if len(rows) == len(ref_rows):
524
+ bd["row_count_correct"] = 0.25
525
+ elif abs(len(rows) - len(ref_rows)) <= 1:
526
+ bd["row_count_close"] = 0.10
527
+
528
+ # 3. Column names match
529
+ if rows and ref_rows:
530
+ pred_cols = set(rows[0].keys())
531
+ ref_cols = set(ref_rows[0].keys())
532
+ col_overlap = len(pred_cols & ref_cols) / max(len(ref_cols), 1)
533
+ bd["columns"] = round(col_overlap * 0.15, 4)
534
+
535
+ # 4. Data correctness — compare sorted string representations
536
+ if rows and ref_rows and len(rows) == len(ref_rows):
537
+ pred_str = sorted(str(sorted(r.items())) for r in rows)
538
+ ref_str = sorted(str(sorted(r.items())) for r in ref_rows)
539
+ if pred_str == ref_str:
540
+ bd["data_exact"] = 0.30
541
+ else:
542
+ # Partial: at least first-column values match
543
+ pred_first = sorted(str(list(r.values())[0]) for r in rows)
544
+ ref_first = sorted(str(list(r.values())[0]) for r in ref_rows)
545
+ if pred_first == ref_first:
546
+ bd["data_partial"] = 0.15
547
+
548
+ # 5. Security grading (hard task: injection)
549
+ if challenge.id == "sq008":
550
+ sql_lower = fixed_sql.lower()
551
+ injection_pattern = "dept"
552
+ if injection_pattern not in fixed_sql and "concat" not in sql_lower:
553
+ bd["injection_removed"] = 0.10
554
+ # Reward parameterized hint in explanation
555
+ if action.explanation and any(
556
+ kw in action.explanation.lower()
557
+ for kw in ["parameterized", "prepared", "placeholder", "?", "injection"]
558
+ ):
559
+ bd["security_explanation"] = 0.05
560
+
561
+ # 6. Optimization grading (hard task: N+1)
562
+ if challenge.id == "sq009":
563
+ sql_lower = fixed_sql.lower()
564
+ # Penalize correlated subquery pattern still present
565
+ if "select" in sql_lower[sql_lower.find("select")+6:]: # nested SELECT
566
+ subq_count = sql_lower.count("select")
567
+ if subq_count <= 2: # CTE or single subquery = OK
568
+ bd["optimization_ok"] = 0.05
569
+ else:
570
+ bd["n_plus_1_penalty"] = -0.10
571
+ if "join" in sql_lower:
572
+ bd["uses_join"] = 0.05
573
+
574
+ # 7. Explanation quality bonus
575
+ if action.explanation and len(action.explanation.split()) >= 8:
576
+ bd["explanation_bonus"] = 0.05
577
+
578
+ # 8. Bug detection quality (hard task)
579
+ if self.task == "hard" and action.detected_issues:
580
+ bd["issues_detected"] = min(0.05, len(action.detected_issues) * 0.02)
581
+
582
+ return bd
583
+
584
+ # ------------------------------------------------------------------
585
+ # Helpers
586
+ # ------------------------------------------------------------------
587
+
588
+ def _find_challenge(self, cid: str) -> Optional[SQLChallenge]:
589
+ for c in self._challenges:
590
+ if c.id == cid:
591
+ return c
592
+ return None
593
+
594
+ def _challenge_index(self, cid: str) -> Optional[int]:
595
+ for i, c in enumerate(self._challenges):
596
+ if c.id == cid:
597
+ return i
598
+ return None
599
+
600
+ def _make_observation(self, idx: int) -> Observation:
601
+ challenge = self._challenges[min(idx, len(self._challenges) - 1)]
602
+ return Observation(
603
+ challenge=challenge,
604
+ schema_info=SCHEMA_INFO,
605
+ current_step=self._step,
606
+ max_steps=self._cfg["max_steps"],
607
+ task=self.task,
608
+ previous_attempts=[
609
+ h for h in self._history
610
+ if h["challenge_id"] == challenge.id
611
+ ],
612
+ instructions=INSTRUCTIONS[self.task],
613
+ )
uv.lock ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10, <3.12"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.11'",
6
+ "python_full_version < '3.11'",
7
+ ]
8
+
9
+ [[package]]
10
+ name = "annotated-doc"
11
+ version = "0.0.4"
12
+ source = { registry = "https://pypi.org/simple" }
13
+ sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "annotated-types"
20
+ version = "0.7.0"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
25
+ ]
26
+
27
+ [[package]]
28
+ name = "anyio"
29
+ version = "4.13.0"
30
+ source = { registry = "https://pypi.org/simple" }
31
+ dependencies = [
32
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
33
+ { name = "idna" },
34
+ { name = "typing-extensions" },
35
+ ]
36
+ sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
37
+ wheels = [
38
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "click"
43
+ version = "8.3.2"
44
+ source = { registry = "https://pypi.org/simple" }
45
+ dependencies = [
46
+ { name = "colorama", marker = "sys_platform == 'win32'" },
47
+ ]
48
+ sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "colorama"
55
+ version = "0.4.6"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "exceptiongroup"
64
+ version = "1.3.1"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ dependencies = [
67
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
68
+ ]
69
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
70
+ wheels = [
71
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
72
+ ]
73
+
74
+ [[package]]
75
+ name = "fastapi"
76
+ version = "0.135.3"
77
+ source = { registry = "https://pypi.org/simple" }
78
+ dependencies = [
79
+ { name = "annotated-doc" },
80
+ { name = "pydantic" },
81
+ { name = "starlette" },
82
+ { name = "typing-extensions" },
83
+ { name = "typing-inspection" },
84
+ ]
85
+ sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" }
86
+ wheels = [
87
+ { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" },
88
+ ]
89
+
90
+ [[package]]
91
+ name = "h11"
92
+ version = "0.16.0"
93
+ source = { registry = "https://pypi.org/simple" }
94
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
95
+ wheels = [
96
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
97
+ ]
98
+
99
+ [[package]]
100
+ name = "idna"
101
+ version = "3.11"
102
+ source = { registry = "https://pypi.org/simple" }
103
+ sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
104
+ wheels = [
105
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
106
+ ]
107
+
108
+ [[package]]
109
+ name = "numpy"
110
+ version = "2.2.6"
111
+ source = { registry = "https://pypi.org/simple" }
112
+ resolution-markers = [
113
+ "python_full_version < '3.11'",
114
+ ]
115
+ sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
116
+ wheels = [
117
+ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
118
+ { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
119
+ { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
120
+ { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
121
+ { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
122
+ { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
123
+ { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
124
+ { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
125
+ { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
126
+ { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
127
+ { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
128
+ { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
129
+ { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
130
+ { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
131
+ { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
132
+ { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
133
+ { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
134
+ { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
135
+ { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
136
+ { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
137
+ { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
138
+ { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
139
+ { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
140
+ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
141
+ ]
142
+
143
+ [[package]]
144
+ name = "numpy"
145
+ version = "2.4.4"
146
+ source = { registry = "https://pypi.org/simple" }
147
+ resolution-markers = [
148
+ "python_full_version >= '3.11'",
149
+ ]
150
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
151
+ wheels = [
152
+ { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" },
153
+ { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" },
154
+ { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" },
155
+ { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" },
156
+ { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" },
157
+ { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" },
158
+ { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" },
159
+ { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" },
160
+ { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" },
161
+ { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" },
162
+ { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" },
163
+ { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" },
164
+ { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" },
165
+ { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" },
166
+ { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" },
167
+ { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" },
168
+ { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" },
169
+ { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
170
+ ]
171
+
172
+ [[package]]
173
+ name = "openenv"
174
+ version = "0.1.13"
175
+ source = { registry = "https://pypi.org/simple" }
176
+ dependencies = [
177
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
178
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
179
+ ]
180
+ sdist = { url = "https://files.pythonhosted.org/packages/35/94/c47e8f7303452793a3519c8cbc1b31dfffdedd13aaed821958ab3f152927/openenv-0.1.13.tar.gz", hash = "sha256:726971d2289472c1c20261436bcccdf3edfcf0b201d16aec127815bd83bfcb3d", size = 5112, upload-time = "2020-12-16T11:49:39.777Z" }
181
+ wheels = [
182
+ { url = "https://files.pythonhosted.org/packages/33/7f/e6f4467528161b8f0eb2ec784f4bbcd1fa9ea7acad13c0fb18597013e83b/openenv-0.1.13-py3-none-any.whl", hash = "sha256:813249d7f526f40c6e8b325f705294761a5bc887b9144c3383fa2bae7baa7726", size = 12080, upload-time = "2020-12-16T11:49:38.816Z" },
183
+ ]
184
+
185
+ [[package]]
186
+ name = "pydantic"
187
+ version = "2.12.5"
188
+ source = { registry = "https://pypi.org/simple" }
189
+ dependencies = [
190
+ { name = "annotated-types" },
191
+ { name = "pydantic-core" },
192
+ { name = "typing-extensions" },
193
+ { name = "typing-inspection" },
194
+ ]
195
+ sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
196
+ wheels = [
197
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
198
+ ]
199
+
200
+ [[package]]
201
+ name = "pydantic-core"
202
+ version = "2.41.5"
203
+ source = { registry = "https://pypi.org/simple" }
204
+ dependencies = [
205
+ { name = "typing-extensions" },
206
+ ]
207
+ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
208
+ wheels = [
209
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
210
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
211
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
212
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
213
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
214
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
215
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
216
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
217
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
218
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
219
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
220
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
221
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
222
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
223
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
224
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
225
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
226
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
227
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
228
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
229
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
230
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
231
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
232
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
233
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
234
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
235
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
236
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
237
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
238
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
239
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
240
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
241
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
242
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
243
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
244
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
245
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
246
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
247
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
248
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
249
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
250
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
251
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
252
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
253
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
254
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
255
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
256
+ ]
257
+
258
+ [[package]]
259
+ name = "sql-debugger-env"
260
+ version = "0.1.0"
261
+ source = { editable = "." }
262
+ dependencies = [
263
+ { name = "fastapi" },
264
+ { name = "openenv" },
265
+ { name = "pydantic" },
266
+ { name = "uvicorn" },
267
+ ]
268
+
269
+ [package.metadata]
270
+ requires-dist = [
271
+ { name = "fastapi" },
272
+ { name = "openenv" },
273
+ { name = "pydantic" },
274
+ { name = "uvicorn" },
275
+ ]
276
+
277
+ [[package]]
278
+ name = "starlette"
279
+ version = "1.0.0"
280
+ source = { registry = "https://pypi.org/simple" }
281
+ dependencies = [
282
+ { name = "anyio" },
283
+ { name = "typing-extensions" },
284
+ ]
285
+ sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
286
+ wheels = [
287
+ { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
288
+ ]
289
+
290
+ [[package]]
291
+ name = "typing-extensions"
292
+ version = "4.15.0"
293
+ source = { registry = "https://pypi.org/simple" }
294
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
295
+ wheels = [
296
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
297
+ ]
298
+
299
+ [[package]]
300
+ name = "typing-inspection"
301
+ version = "0.4.2"
302
+ source = { registry = "https://pypi.org/simple" }
303
+ dependencies = [
304
+ { name = "typing-extensions" },
305
+ ]
306
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
307
+ wheels = [
308
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
309
+ ]
310
+
311
+ [[package]]
312
+ name = "uvicorn"
313
+ version = "0.44.0"
314
+ source = { registry = "https://pypi.org/simple" }
315
+ dependencies = [
316
+ { name = "click" },
317
+ { name = "h11" },
318
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
319
+ ]
320
+ sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
321
+ wheels = [
322
+ { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
323
+ ]