webtap-tool 0.7.1__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of webtap-tool might be problematic. Click here for more details.

@@ -0,0 +1,110 @@
1
+ """Code generation utilities for transforming HTTP bodies into code.
2
+
3
+ Pure transformation functions with no dependencies on services or state.
4
+ Used by to_model(), quicktype(), and future code generation commands.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ def parse_json(content: str) -> tuple[Any, str | None]:
13
+ """Parse JSON string into Python object.
14
+
15
+ Args:
16
+ content: JSON string to parse.
17
+
18
+ Returns:
19
+ Tuple of (parsed_data, error_message).
20
+ On success: (data, None)
21
+ On failure: (None, error_string)
22
+
23
+ Examples:
24
+ data, error = parse_json('{"key": "value"}')
25
+ if error:
26
+ return error_response(error)
27
+ """
28
+ try:
29
+ return json.loads(content), None
30
+ except json.JSONDecodeError as e:
31
+ return None, f"Invalid JSON: {e}"
32
+
33
+
34
+ def extract_json_path(data: Any, path: str) -> tuple[Any, str | None]:
35
+ """Extract nested data using simple bracket notation.
36
+
37
+ Supports paths like "data[0]", "results.users", or "data[0].items".
38
+
39
+ Args:
40
+ data: Dict or list to extract from.
41
+ path: Path using dot and bracket notation.
42
+
43
+ Returns:
44
+ Tuple of (extracted_data, error_message).
45
+ On success: (data, None)
46
+ On failure: (None, error_string)
47
+
48
+ Examples:
49
+ result, err = extract_json_path({"data": [1,2,3]}, "data[0]")
50
+ # result = 1, err = None
51
+
52
+ result, err = extract_json_path({"user": {"name": "Bob"}}, "user.name")
53
+ # result = "Bob", err = None
54
+ """
55
+ try:
56
+ parts = path.replace("[", ".").replace("]", "").split(".")
57
+ result = data
58
+ for part in parts:
59
+ if part:
60
+ if part.isdigit():
61
+ result = result[int(part)]
62
+ else:
63
+ result = result[part]
64
+ return result, None
65
+ except (KeyError, IndexError, TypeError) as e:
66
+ return None, f"JSON path '{path}' not found: {e}"
67
+
68
+
69
+ def validate_generation_data(data: Any) -> tuple[bool, str | None]:
70
+ """Validate data structure for code generation.
71
+
72
+ Code generators (Pydantic, quicktype) require dict or list structures.
73
+
74
+ Args:
75
+ data: Data to validate.
76
+
77
+ Returns:
78
+ Tuple of (is_valid, error_message).
79
+ On success: (True, None)
80
+ On failure: (False, error_string)
81
+
82
+ Examples:
83
+ is_valid, error = validate_generation_data({"key": "value"})
84
+ # is_valid = True, error = None
85
+
86
+ is_valid, error = validate_generation_data("string")
87
+ # is_valid = False, error = "Data is str, not dict or list"
88
+ """
89
+ if not isinstance(data, (dict, list)):
90
+ return False, f"Data is {type(data).__name__}, not dict or list"
91
+ return True, None
92
+
93
+
94
+ def ensure_output_directory(output: str) -> Path:
95
+ """Create output directory if needed, return resolved path.
96
+
97
+ Args:
98
+ output: Output file path (can be relative, use ~, etc.).
99
+
100
+ Returns:
101
+ Resolved absolute Path object.
102
+
103
+ Examples:
104
+ path = ensure_output_directory("~/models/user.py")
105
+ # Creates ~/models/ if it doesn't exist
106
+ # Returns Path("/home/user/models/user.py")
107
+ """
108
+ output_path = Path(output).expanduser().resolve()
109
+ output_path.parent.mkdir(parents=True, exist_ok=True)
110
+ return output_path
webtap/commands/body.py CHANGED
@@ -11,11 +11,15 @@ mcp_desc = get_mcp_description("body")
11
11
 
12
12
 
13
13
  @app.command(display="markdown", fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"})
14
- def body(state, response: int, expr: str = None, decode: bool = True, cache: bool = True) -> dict: # pyright: ignore[reportArgumentType]
15
- """Fetch and analyze response body with Python expressions.
14
+ def body(state, event: int, expr: str = None, decode: bool = True, cache: bool = True) -> dict: # pyright: ignore[reportArgumentType]
15
+ """Fetch and analyze request or response body with Python expressions.
16
+
17
+ Automatically detects event type and fetches appropriate body:
18
+ - Request events (Network.requestWillBeSent): POST/PUT/PATCH request body
19
+ - Response events (Network.responseReceived): response body
16
20
 
17
21
  Args:
18
- response: Response row ID from network() or requests()
22
+ event: Event row ID from network(), events(), or requests()
19
23
  expr: Optional Python expression with 'body' variable
20
24
  decode: Auto-decode base64 (default: True)
21
25
  cache: Use cached body (default: True)
@@ -28,7 +32,7 @@ def body(state, response: int, expr: str = None, decode: bool = True, cache: boo
28
32
 
29
33
  # Get body from service (with optional caching)
30
34
  body_service = state.service.body
31
- result = body_service.get_response_body(response, use_cache=cache)
35
+ result = body_service.get_body(event, use_cache=cache)
32
36
 
33
37
  if "error" in result:
34
38
  return error_response(result["error"])
@@ -66,7 +70,7 @@ def body(state, response: int, expr: str = None, decode: bool = True, cache: boo
66
70
  # Build markdown response with body in code block
67
71
  # DATA-LEVEL TRUNCATION for memory/performance (as per refactor plan)
68
72
  MAX_BODY_SIZE = 5000 # Keep data-level truncation for large bodies
69
- elements = [{"type": "heading", "content": "Response Body", "level": 2}]
73
+ elements = [{"type": "heading", "content": "Body", "level": 2}]
70
74
 
71
75
  # Try to detect content type and format appropriately
72
76
  content_preview = body_content[:100]
@@ -2,6 +2,7 @@
2
2
 
3
3
  from webtap.app import app
4
4
  from webtap.commands._builders import check_connection, info_response, table_response, error_response
5
+ from webtap.commands._tips import get_tips
5
6
 
6
7
 
7
8
  @app.command(display="markdown", fastmcp={"type": "tool"})
@@ -132,12 +133,32 @@ def pages(state) -> dict:
132
133
  for i, p in enumerate(pages_list)
133
134
  ]
134
135
 
136
+ # Get contextual tips
137
+ tips = None
138
+ if rows:
139
+ # Find connected page or first page
140
+ connected_row = next((r for r in rows if r["Connected"] == "Yes"), rows[0])
141
+ page_index = connected_row["Index"]
142
+
143
+ # Get page_id for the example page
144
+ connected_page = next((p for p in pages_list if str(pages_list.index(p)) == page_index), None)
145
+ page_id = connected_page.get("id", "")[:6] if connected_page else ""
146
+
147
+ tips = get_tips("pages", context={"index": page_index, "page_id": page_id})
148
+
149
+ # Build contextual warnings
150
+ warnings = []
151
+ if any(r["Connected"] == "Yes" for r in rows):
152
+ warnings.append("Already connected - call connect(page=N) to switch pages")
153
+
135
154
  # Build markdown response
136
155
  return table_response(
137
156
  title="Chrome Pages",
138
157
  headers=["Index", "Title", "URL", "ID", "Connected"],
139
158
  rows=rows,
140
159
  summary=f"{len(pages_list)} page{'s' if len(pages_list) != 1 else ''} available",
160
+ warnings=warnings if warnings else None,
161
+ tips=tips,
141
162
  )
142
163
 
143
164
 
@@ -23,39 +23,27 @@ def js(
23
23
  wait_return: bool = True,
24
24
  await_promise: bool = False,
25
25
  ) -> dict:
26
- """Execute JavaScript in the browser with optional element selection.
26
+ """Execute JavaScript in the browser.
27
+
28
+ Uses fresh scope by default to avoid redeclaration errors. Set persist=True
29
+ to keep variables across calls. Use selection=N to operate on browser elements.
27
30
 
28
31
  Args:
29
- code: JavaScript code to execute (use 'element' variable if selection provided)
30
- selection: Browser element selection number (e.g., 1 for #1) - makes element available
31
- persist: Keep variables in global scope across calls (default: False, uses fresh scope)
32
+ code: JavaScript code to execute
33
+ selection: Browser element selection number - makes 'element' variable available
34
+ persist: Keep variables in global scope across calls (default: False)
32
35
  wait_return: Wait for and return result (default: True)
33
36
  await_promise: Await promises before returning (default: False)
34
37
 
35
38
  Examples:
36
- # Default: fresh scope (no redeclaration errors)
37
- js("const x = 1") # x isolated
38
- js("const x = 2") # No error, fresh scope
39
- js("document.title") # Get page title
40
- js("[...document.links].map(a => a.href)") # Get all links
41
-
42
- # With browser element selection (always fresh scope)
43
- js("element.offsetWidth", selection=1) # Use element #1
44
- js("element.classList", selection=2) # Use element #2
45
- js("element.getBoundingClientRect()", selection=1)
46
-
47
- # Persistent scope: keep variables across calls
48
- js("var data = fetch('/api')", persist=True) # data persists
49
- js("data.json()", persist=True) # Access data from previous call
50
-
51
- # Async operations
52
- js("fetch('/api').then(r => r.json())", await_promise=True)
53
-
54
- # DOM manipulation (no return needed)
55
- js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
39
+ js("document.title") # Fresh scope (default)
40
+ js("var data = {count: 0}", persist=True) # Persistent state
41
+ js("element.offsetWidth", selection=1) # With browser element
42
+ js("fetch('/api')", await_promise=True) # Async operation
43
+ js("element.remove()", selection=1, wait_return=False) # No return needed
56
44
 
57
45
  Returns:
58
- The evaluated result if wait_return=True, otherwise execution status
46
+ Evaluated result if wait_return=True, otherwise execution status
59
47
  """
60
48
  if error := check_connection(state):
61
49
  return error
@@ -2,6 +2,7 @@
2
2
 
3
3
  from webtap.app import app
4
4
  from webtap.commands._builders import check_connection, info_response, table_response, error_response
5
+ from webtap.commands._tips import get_tips
5
6
 
6
7
 
7
8
  @app.command(display="markdown", fastmcp={"type": "tool"})
@@ -146,6 +147,9 @@ def page(state) -> dict:
146
147
  except Exception:
147
148
  title = current.get("title", "")
148
149
 
150
+ # Get tips from TIPS.md
151
+ tips = get_tips("page")
152
+
149
153
  # Build formatted response
150
154
  return info_response(
151
155
  title=title or "Untitled Page",
@@ -154,6 +158,7 @@ def page(state) -> dict:
154
158
  "ID": current.get("id", ""),
155
159
  "Type": current.get("transitionType", ""),
156
160
  },
161
+ tips=tips,
157
162
  )
158
163
 
159
164
  return error_response("No navigation history available")
@@ -0,0 +1,268 @@
1
+ """Generate types and schemas from HTTP response bodies using quicktype."""
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from webtap.app import app
9
+ from webtap.commands._builders import check_connection, success_response, error_response
10
+ from webtap.commands._code_generation import ensure_output_directory
11
+ from webtap.commands._tips import get_mcp_description
12
+
13
+
14
+ mcp_desc = get_mcp_description("quicktype")
15
+
16
+ # Header template for generated files
17
+ HEADER_TEMPLATE = """Generated by WebTap from event {event_id}
18
+ Source: {url}
19
+ Method: {method}
20
+ Generated: {timestamp}
21
+
22
+ Do not edit manually."""
23
+
24
+ # Comment syntax per language
25
+ COMMENT_STYLES = {
26
+ "TypeScript": "//",
27
+ "Python": "#",
28
+ "Go": "//",
29
+ "Rust": "//",
30
+ "Java": "//",
31
+ "Kotlin": "//",
32
+ "Swift": "//",
33
+ "C#": "//",
34
+ "C++": "//",
35
+ "Dart": "//",
36
+ "Ruby": "#",
37
+ "JSON Schema": None, # No comments in JSON
38
+ }
39
+
40
+
41
+ def _run_quicktype(
42
+ json_data: dict | list,
43
+ output: str,
44
+ type_name: str = None, # pyright: ignore[reportArgumentType]
45
+ just_types: bool = True,
46
+ prefer_types: bool = True,
47
+ options: dict = None, # pyright: ignore[reportArgumentType]
48
+ ) -> tuple[bool, str]:
49
+ """Run quicktype CLI with given parameters.
50
+
51
+ Args:
52
+ json_data: JSON data to convert
53
+ output: Output file path
54
+ type_name: Top-level type name
55
+ just_types: Generate only types, no serializers
56
+ prefer_types: Use 'type' instead of 'interface' for TypeScript
57
+ options: Additional quicktype flags
58
+
59
+ Returns:
60
+ Tuple of (success: bool, error_message: str)
61
+ """
62
+ # Check if quicktype is available
63
+ if not shutil.which("quicktype"):
64
+ return False, "quicktype CLI not found. Install with: npm install -g quicktype"
65
+
66
+ # Determine language from file extension
67
+ output_path = Path(output)
68
+ ext = output_path.suffix.lower()
69
+
70
+ # Build command
71
+ cmd = ["quicktype", "-o", str(output), "--src-lang", "json", "--top-level", type_name]
72
+
73
+ # Add opinionated defaults
74
+ if just_types:
75
+ cmd.append("--just-types")
76
+
77
+ # TypeScript-specific options
78
+ if ext in {".ts", ".tsx"} and prefer_types:
79
+ cmd.append("--prefer-types")
80
+
81
+ # Apply additional options
82
+ for key, val in (options or {}).items():
83
+ flag = f"--{key.replace('_', '-')}"
84
+ if val is True:
85
+ cmd.append(flag)
86
+ elif val is not False and val is not None:
87
+ cmd.extend([flag, str(val)])
88
+
89
+ # Run quicktype
90
+ try:
91
+ subprocess.run(
92
+ cmd,
93
+ input=json.dumps(json_data, indent=2),
94
+ capture_output=True,
95
+ text=True,
96
+ check=True,
97
+ timeout=30,
98
+ )
99
+ return True, ""
100
+ except subprocess.CalledProcessError as e:
101
+ return False, f"quicktype failed: {e.stderr.strip() if e.stderr else str(e)}"
102
+ except subprocess.TimeoutExpired:
103
+ return False, "quicktype timed out (>30s)"
104
+ except Exception as e:
105
+ return False, f"Unexpected error: {str(e)}"
106
+
107
+
108
+ def _insert_header(state, event: int, output_path: Path, language: str) -> None:
109
+ """Insert language-aware header comment into generated file.
110
+
111
+ Args:
112
+ state: WebTap state with CDP session
113
+ event: Event row ID for metadata extraction
114
+ output_path: Path to generated file
115
+ language: Target language (e.g., "TypeScript", "Python")
116
+ """
117
+ if not HEADER_TEMPLATE or language not in COMMENT_STYLES:
118
+ return
119
+
120
+ comment_prefix = COMMENT_STYLES[language]
121
+ if not comment_prefix: # Skip languages without comments (e.g., JSON)
122
+ return
123
+
124
+ try:
125
+ # Get event metadata
126
+ event_result = state.cdp.query("SELECT event FROM events WHERE rowid = ?", [event])
127
+ if not event_result:
128
+ return
129
+
130
+ event_data = json.loads(event_result[0][0])
131
+ params = event_data.get("params", {})
132
+
133
+ # Extract metadata from event
134
+ request_data = params.get("request", {})
135
+ response_data = params.get("response", {})
136
+
137
+ metadata = {
138
+ "event_id": event,
139
+ "url": request_data.get("url") or response_data.get("url", "N/A"),
140
+ "method": request_data.get("method", "N/A"),
141
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
142
+ }
143
+
144
+ # Format header text with metadata
145
+ header_text = HEADER_TEMPLATE.format(**metadata)
146
+
147
+ # Apply comment syntax to each line
148
+ header_lines = [
149
+ f"{comment_prefix} {line}" if line.strip() else comment_prefix for line in header_text.split("\n")
150
+ ]
151
+ header = "\n".join(header_lines)
152
+
153
+ # Prepend header to file
154
+ content = output_path.read_text()
155
+ output_path.write_text(header + "\n\n" + content)
156
+ except Exception:
157
+ # Silent failure - don't break generation if header fails
158
+ pass
159
+
160
+
161
+ @app.command(display="markdown", fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"})
162
+ def quicktype(
163
+ state,
164
+ event: int,
165
+ output: str,
166
+ type_name: str,
167
+ json_path: str = None, # pyright: ignore[reportArgumentType]
168
+ expr: str = None, # pyright: ignore[reportArgumentType]
169
+ just_types: bool = True,
170
+ prefer_types: bool = True,
171
+ options: dict = None, # pyright: ignore[reportArgumentType]
172
+ ) -> dict: # pyright: ignore[reportArgumentType]
173
+ """Generate types/schemas from request or response body using quicktype CLI.
174
+
175
+ Args:
176
+ event: Event row ID from network() or events()
177
+ output: Output file path (extension determines language: .ts, .py, .go, etc.)
178
+ type_name: Top-level type name (e.g., "User", "ApiResponse")
179
+ json_path: Optional JSON path to extract nested data (e.g., "data[0]")
180
+ expr: Optional Python expression to transform data (has 'body' and 'event' variables)
181
+ just_types: Generate only types, no serializers (default: True)
182
+ prefer_types: Use 'type' instead of 'interface' for TypeScript (default: True)
183
+ options: Additional quicktype flags as dict (e.g., {"readonly": True, "nice_property_names": True})
184
+
185
+ Examples:
186
+ quicktype(123, "types/User.ts", "User") # TypeScript interface
187
+ quicktype(123, "models/customer.py", "Customer") # Python dataclass
188
+ quicktype(123, "api.go", "ApiResponse") # Go struct
189
+ quicktype(123, "schema.json", "Schema") # JSON Schema
190
+ quicktype(123, "types.ts", "User", json_path="data[0]") # Extract nested
191
+ quicktype(172, "types/Form.ts", "Form", expr="dict(urllib.parse.parse_qsl(body))") # Parse form data
192
+ quicktype(123, "types.ts", "User", options={"readonly": True}) # Advanced options
193
+
194
+ Returns:
195
+ Success message with generation details
196
+ """
197
+ if error := check_connection(state):
198
+ return error
199
+
200
+ # Prepare data via service layer
201
+ result = state.service.body.prepare_for_generation(event, json_path, expr)
202
+ if result.get("error"):
203
+ return error_response(result["error"], suggestions=result.get("suggestions", []))
204
+
205
+ data = result["data"]
206
+
207
+ # Ensure output directory exists
208
+ output_path = ensure_output_directory(output)
209
+
210
+ # Run quicktype
211
+ success, error_msg = _run_quicktype(
212
+ json_data=data,
213
+ output=str(output_path),
214
+ type_name=type_name,
215
+ just_types=just_types,
216
+ prefer_types=prefer_types,
217
+ options=options,
218
+ )
219
+
220
+ if not success:
221
+ return error_response(
222
+ error_msg,
223
+ suggestions=[
224
+ "Check that quicktype is installed: npm install -g quicktype",
225
+ "Verify the JSON structure is valid",
226
+ "Try simplifying the data with json_path",
227
+ ],
228
+ )
229
+
230
+ # Detect language from extension
231
+ ext = output_path.suffix.lower()
232
+ lang_map = {
233
+ ".ts": "TypeScript",
234
+ ".tsx": "TypeScript",
235
+ ".py": "Python",
236
+ ".go": "Go",
237
+ ".rs": "Rust",
238
+ ".java": "Java",
239
+ ".kt": "Kotlin",
240
+ ".swift": "Swift",
241
+ ".cs": "C#",
242
+ ".cpp": "C++",
243
+ ".dart": "Dart",
244
+ ".rb": "Ruby",
245
+ ".json": "JSON Schema",
246
+ }
247
+ language = lang_map.get(ext, "Unknown")
248
+
249
+ # Add header comment with event metadata
250
+ _insert_header(state, event, output_path, language)
251
+
252
+ # Count lines in generated file
253
+ try:
254
+ file_content = output_path.read_text()
255
+ line_count = len(file_content.splitlines())
256
+ except Exception:
257
+ line_count = "unknown"
258
+
259
+ return success_response(
260
+ "Types generated successfully",
261
+ details={
262
+ "Output": str(output_path),
263
+ "Language": language,
264
+ "Type Name": type_name,
265
+ "Lines": line_count,
266
+ "Size": f"{output_path.stat().st_size} bytes",
267
+ },
268
+ )
@@ -1,10 +1,10 @@
1
1
  """Generate Pydantic models from HTTP response bodies."""
2
2
 
3
3
  import json
4
- from pathlib import Path
5
4
  from datamodel_code_generator import generate, InputFileType, DataModelType
6
5
  from webtap.app import app
7
6
  from webtap.commands._builders import check_connection, success_response, error_response
7
+ from webtap.commands._code_generation import ensure_output_directory
8
8
  from webtap.commands._tips import get_mcp_description
9
9
 
10
10
 
@@ -12,14 +12,20 @@ mcp_desc = get_mcp_description("to_model")
12
12
 
13
13
 
14
14
  @app.command(display="markdown", fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"})
15
- def to_model(state, response: int, output: str, model_name: str, json_path: str = None) -> dict: # pyright: ignore[reportArgumentType]
16
- """Generate Pydantic model from response body using datamodel-codegen.
15
+ def to_model(state, event: int, output: str, model_name: str, json_path: str = None, expr: str = None) -> dict: # pyright: ignore[reportArgumentType]
16
+ """Generate Pydantic model from request or response body using datamodel-codegen.
17
17
 
18
18
  Args:
19
- response: Response row ID from network() table
19
+ event: Event row ID from network() or events()
20
20
  output: Output file path for generated model (e.g., "models/customers/group.py")
21
21
  model_name: Class name for generated model (e.g., "CustomerGroup")
22
- json_path: Optional JSON path to extract nested data (e.g., "Data[0]")
22
+ json_path: Optional JSON path to extract nested data (e.g., "data[0]")
23
+ expr: Optional Python expression to transform data (has 'body' and 'event' variables)
24
+
25
+ Examples:
26
+ to_model(123, "models/user.py", "User", json_path="data[0]")
27
+ to_model(172, "models/form.py", "Form", expr="dict(urllib.parse.parse_qsl(body))")
28
+ to_model(123, "models/clean.py", "Clean", expr="{k: v for k, v in json.loads(body).items() if k != 'meta'}")
23
29
 
24
30
  Returns:
25
31
  Success message with generation details
@@ -27,73 +33,15 @@ def to_model(state, response: int, output: str, model_name: str, json_path: str
27
33
  if error := check_connection(state):
28
34
  return error
29
35
 
30
- # Get response body from service
31
- body_service = state.service.body
32
- result = body_service.get_response_body(response, use_cache=True)
33
-
34
- if "error" in result:
35
- return error_response(result["error"])
36
-
37
- body_content = result.get("body", "")
38
- is_base64 = result.get("base64Encoded", False)
39
-
40
- # Decode if needed
41
- if is_base64:
42
- decoded = body_service.decode_body(body_content, is_base64)
43
- if isinstance(decoded, bytes):
44
- return error_response(
45
- "Response body is binary",
46
- suggestions=["Only JSON responses can be converted to models", "Try a different response"],
47
- )
48
- body_content = decoded
36
+ # Prepare data via service layer
37
+ result = state.service.body.prepare_for_generation(event, json_path, expr)
38
+ if result.get("error"):
39
+ return error_response(result["error"], suggestions=result.get("suggestions", []))
49
40
 
50
- # Parse JSON
51
- try:
52
- data = json.loads(body_content)
53
- except json.JSONDecodeError as e:
54
- return error_response(
55
- f"Invalid JSON: {e}",
56
- suggestions=[
57
- "Response must be valid JSON",
58
- "Check the response with body() first",
59
- "Try a different response",
60
- ],
61
- )
62
-
63
- # Extract JSON path if specified
64
- if json_path:
65
- try:
66
- # Support simple bracket notation like "Data[0]"
67
- parts = json_path.replace("[", ".").replace("]", "").split(".")
68
- for part in parts:
69
- if part:
70
- if part.isdigit():
71
- data = data[int(part)]
72
- else:
73
- data = data[part]
74
- except (KeyError, IndexError, TypeError) as e:
75
- return error_response(
76
- f"JSON path extraction failed: {e}",
77
- suggestions=[
78
- f"Path '{json_path}' not found in response",
79
- "Check the response structure with body()",
80
- 'Try a simpler path like "Data" or "Data[0]"',
81
- ],
82
- )
83
-
84
- # Ensure data is dict or list for model generation
85
- if not isinstance(data, (dict, list)):
86
- return error_response(
87
- f"Extracted data is {type(data).__name__}, not dict or list",
88
- suggestions=[
89
- "Model generation requires dict or list structure",
90
- "Adjust json_path to extract a complex object",
91
- ],
92
- )
41
+ data = result["data"]
93
42
 
94
- # Create output directory if needed
95
- output_path = Path(output).expanduser().resolve()
96
- output_path.parent.mkdir(parents=True, exist_ok=True)
43
+ # Ensure output directory exists
44
+ output_path = ensure_output_directory(output)
97
45
 
98
46
  # Generate model using datamodel-codegen Python API
99
47
  try:
@@ -103,10 +51,10 @@ def to_model(state, response: int, output: str, model_name: str, json_path: str
103
51
  input_filename="response.json",
104
52
  output=output_path,
105
53
  output_model_type=DataModelType.PydanticV2BaseModel,
106
- class_name=model_name, # Set generated class name
107
- snake_case_field=True, # Convert to snake_case
108
- use_standard_collections=True, # Use list instead of List
109
- use_union_operator=True, # Use | instead of Union
54
+ class_name=model_name,
55
+ snake_case_field=True,
56
+ use_standard_collections=True,
57
+ use_union_operator=True,
110
58
  )
111
59
  except Exception as e:
112
60
  return error_response(
@@ -121,7 +69,7 @@ def to_model(state, response: int, output: str, model_name: str, json_path: str
121
69
  # Count fields in generated model
122
70
  try:
123
71
  model_content = output_path.read_text()
124
- field_count = model_content.count(": ") # Count field definitions
72
+ field_count = model_content.count(": ")
125
73
  except Exception:
126
74
  field_count = "unknown"
127
75