mcpforunityserver 8.7.0__py3-none-any.whl → 9.1.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.
Files changed (81) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +87 -0
  4. cli/commands/asset.py +310 -0
  5. cli/commands/audio.py +133 -0
  6. cli/commands/batch.py +184 -0
  7. cli/commands/code.py +189 -0
  8. cli/commands/component.py +212 -0
  9. cli/commands/editor.py +487 -0
  10. cli/commands/gameobject.py +510 -0
  11. cli/commands/instance.py +101 -0
  12. cli/commands/lighting.py +128 -0
  13. cli/commands/material.py +268 -0
  14. cli/commands/prefab.py +144 -0
  15. cli/commands/scene.py +255 -0
  16. cli/commands/script.py +240 -0
  17. cli/commands/shader.py +238 -0
  18. cli/commands/ui.py +263 -0
  19. cli/commands/vfx.py +439 -0
  20. cli/main.py +248 -0
  21. cli/utils/__init__.py +31 -0
  22. cli/utils/config.py +58 -0
  23. cli/utils/connection.py +191 -0
  24. cli/utils/output.py +195 -0
  25. main.py +177 -62
  26. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/METADATA +4 -2
  27. mcpforunityserver-9.1.0.dist-info/RECORD +96 -0
  28. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/WHEEL +1 -1
  29. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/entry_points.txt +1 -0
  30. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/top_level.txt +1 -2
  31. services/custom_tool_service.py +179 -19
  32. services/resources/__init__.py +6 -1
  33. services/resources/active_tool.py +1 -1
  34. services/resources/custom_tools.py +2 -2
  35. services/resources/editor_state.py +283 -30
  36. services/resources/gameobject.py +243 -0
  37. services/resources/layers.py +1 -1
  38. services/resources/prefab_stage.py +1 -1
  39. services/resources/project_info.py +1 -1
  40. services/resources/selection.py +1 -1
  41. services/resources/tags.py +1 -1
  42. services/resources/unity_instances.py +1 -1
  43. services/resources/windows.py +1 -1
  44. services/state/external_changes_scanner.py +3 -4
  45. services/tools/__init__.py +6 -1
  46. services/tools/batch_execute.py +24 -9
  47. services/tools/debug_request_context.py +8 -2
  48. services/tools/execute_custom_tool.py +6 -1
  49. services/tools/execute_menu_item.py +6 -3
  50. services/tools/find_gameobjects.py +89 -0
  51. services/tools/find_in_file.py +26 -19
  52. services/tools/manage_asset.py +13 -44
  53. services/tools/manage_components.py +131 -0
  54. services/tools/manage_editor.py +9 -8
  55. services/tools/manage_gameobject.py +115 -79
  56. services/tools/manage_material.py +80 -31
  57. services/tools/manage_prefabs.py +7 -1
  58. services/tools/manage_scene.py +30 -13
  59. services/tools/manage_script.py +62 -19
  60. services/tools/manage_scriptable_object.py +22 -10
  61. services/tools/manage_shader.py +8 -1
  62. services/tools/manage_vfx.py +738 -0
  63. services/tools/preflight.py +15 -12
  64. services/tools/read_console.py +70 -17
  65. services/tools/refresh_unity.py +92 -29
  66. services/tools/run_tests.py +187 -53
  67. services/tools/script_apply_edits.py +15 -7
  68. services/tools/set_active_instance.py +12 -7
  69. services/tools/utils.py +60 -6
  70. transport/legacy/port_discovery.py +2 -2
  71. transport/legacy/unity_connection.py +129 -26
  72. transport/plugin_hub.py +85 -24
  73. transport/unity_instance_middleware.py +4 -3
  74. transport/unity_transport.py +2 -1
  75. utils/focus_nudge.py +321 -0
  76. __init__.py +0 -0
  77. mcpforunityserver-8.7.0.dist-info/RECORD +0 -71
  78. routes/__init__.py +0 -0
  79. services/resources/editor_state_v2.py +0 -270
  80. services/tools/test_jobs.py +0 -94
  81. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/licenses/LICENSE +0 -0
cli/commands/batch.py ADDED
@@ -0,0 +1,184 @@
1
+ """Batch CLI commands for executing multiple Unity operations efficiently."""
2
+
3
+ import sys
4
+ import json
5
+ import click
6
+ from typing import Optional, Any
7
+
8
+ from cli.utils.config import get_config
9
+ from cli.utils.output import format_output, print_error, print_success, print_info
10
+ from cli.utils.connection import run_command, UnityConnectionError
11
+
12
+
13
+ @click.group()
14
+ def batch():
15
+ """Batch operations - execute multiple commands efficiently."""
16
+ pass
17
+
18
+
19
+ @batch.command("run")
20
+ @click.argument("file", type=click.Path(exists=True))
21
+ @click.option("--parallel", is_flag=True, help="Execute read-only commands in parallel.")
22
+ @click.option("--fail-fast", is_flag=True, help="Stop on first failure.")
23
+ def batch_run(file: str, parallel: bool, fail_fast: bool):
24
+ """Execute commands from a JSON file.
25
+
26
+ The JSON file should contain an array of command objects with 'tool' and 'params' keys.
27
+
28
+ \\b
29
+ File format:
30
+ [
31
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube1"}},
32
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube2"}},
33
+ {"tool": "manage_components", "params": {"action": "add", "target": "Cube1", "componentType": "Rigidbody"}}
34
+ ]
35
+
36
+ \\b
37
+ Examples:
38
+ unity-mcp batch run commands.json
39
+ unity-mcp batch run setup.json --parallel
40
+ unity-mcp batch run critical.json --fail-fast
41
+ """
42
+ config = get_config()
43
+
44
+ try:
45
+ with open(file, 'r') as f:
46
+ commands = json.load(f)
47
+ except json.JSONDecodeError as e:
48
+ print_error(f"Invalid JSON in file: {e}")
49
+ sys.exit(1)
50
+ except IOError as e:
51
+ print_error(f"Error reading file: {e}")
52
+ sys.exit(1)
53
+
54
+ if not isinstance(commands, list):
55
+ print_error("JSON file must contain an array of commands")
56
+ sys.exit(1)
57
+
58
+ if len(commands) > 25:
59
+ print_error(f"Maximum 25 commands per batch, got {len(commands)}")
60
+ sys.exit(1)
61
+
62
+ params: dict[str, Any] = {"commands": commands}
63
+ if parallel:
64
+ params["parallel"] = True
65
+ if fail_fast:
66
+ params["failFast"] = True
67
+
68
+ click.echo(f"Executing {len(commands)} commands...")
69
+
70
+ try:
71
+ result = run_command("batch_execute", params, config)
72
+ click.echo(format_output(result, config.format))
73
+
74
+ if isinstance(result, dict):
75
+ results = result.get("data", {}).get("results", [])
76
+ succeeded = sum(1 for r in results if r.get("success"))
77
+ failed = len(results) - succeeded
78
+
79
+ if failed == 0:
80
+ print_success(
81
+ f"All {succeeded} commands completed successfully")
82
+ else:
83
+ print_info(f"{succeeded} succeeded, {failed} failed")
84
+ except UnityConnectionError as e:
85
+ print_error(str(e))
86
+ sys.exit(1)
87
+
88
+
89
+ @batch.command("inline")
90
+ @click.argument("commands_json")
91
+ @click.option("--parallel", is_flag=True, help="Execute read-only commands in parallel.")
92
+ @click.option("--fail-fast", is_flag=True, help="Stop on first failure.")
93
+ def batch_inline(commands_json: str, parallel: bool, fail_fast: bool):
94
+ """Execute commands from inline JSON.
95
+
96
+ \\b
97
+ Examples:
98
+ unity-mcp batch inline '[{"tool": "manage_scene", "params": {"action": "get_active"}}]'
99
+
100
+ unity-mcp batch inline '[
101
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "A", "primitiveType": "Cube"}},
102
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "B", "primitiveType": "Sphere"}}
103
+ ]'
104
+ """
105
+ config = get_config()
106
+
107
+ try:
108
+ commands = json.loads(commands_json)
109
+ except json.JSONDecodeError as e:
110
+ print_error(f"Invalid JSON: {e}")
111
+ sys.exit(1)
112
+
113
+ if not isinstance(commands, list):
114
+ print_error("Commands must be an array")
115
+ sys.exit(1)
116
+
117
+ if len(commands) > 25:
118
+ print_error(f"Maximum 25 commands per batch, got {len(commands)}")
119
+ sys.exit(1)
120
+
121
+ params: dict[str, Any] = {"commands": commands}
122
+ if parallel:
123
+ params["parallel"] = True
124
+ if fail_fast:
125
+ params["failFast"] = True
126
+
127
+ try:
128
+ result = run_command("batch_execute", params, config)
129
+ click.echo(format_output(result, config.format))
130
+ except UnityConnectionError as e:
131
+ print_error(str(e))
132
+ sys.exit(1)
133
+
134
+
135
+ @batch.command("template")
136
+ @click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
137
+ def batch_template(output: Optional[str]):
138
+ """Generate a sample batch commands file.
139
+
140
+ \\b
141
+ Examples:
142
+ unity-mcp batch template > commands.json
143
+ unity-mcp batch template -o my_batch.json
144
+ """
145
+ template = [
146
+ {
147
+ "tool": "manage_scene",
148
+ "params": {"action": "get_active"}
149
+ },
150
+ {
151
+ "tool": "manage_gameobject",
152
+ "params": {
153
+ "action": "create",
154
+ "name": "BatchCube",
155
+ "primitiveType": "Cube",
156
+ "position": [0, 1, 0]
157
+ }
158
+ },
159
+ {
160
+ "tool": "manage_components",
161
+ "params": {
162
+ "action": "add",
163
+ "target": "BatchCube",
164
+ "componentType": "Rigidbody"
165
+ }
166
+ },
167
+ {
168
+ "tool": "manage_gameobject",
169
+ "params": {
170
+ "action": "modify",
171
+ "target": "BatchCube",
172
+ "position": [0, 5, 0]
173
+ }
174
+ }
175
+ ]
176
+
177
+ json_output = json.dumps(template, indent=2)
178
+
179
+ if output:
180
+ with open(output, 'w') as f:
181
+ f.write(json_output)
182
+ print_success(f"Template written to: {output}")
183
+ else:
184
+ click.echo(json_output)
cli/commands/code.py ADDED
@@ -0,0 +1,189 @@
1
+ """Code CLI commands - read source code. search might be implemented later (but can be totally achievable with AI)."""
2
+
3
+ import sys
4
+ import os
5
+ import click
6
+ from typing import Optional, Any
7
+
8
+ from cli.utils.config import get_config
9
+ from cli.utils.output import format_output, print_error, print_info
10
+ from cli.utils.connection import run_command, UnityConnectionError
11
+
12
+
13
+ @click.group()
14
+ def code():
15
+ """Code operations - read source files."""
16
+ pass
17
+
18
+
19
+ @code.command("read")
20
+ @click.argument("path")
21
+ @click.option(
22
+ "--start-line", "-s",
23
+ default=None,
24
+ type=int,
25
+ help="Starting line number (1-based)."
26
+ )
27
+ @click.option(
28
+ "--line-count", "-n",
29
+ default=None,
30
+ type=int,
31
+ help="Number of lines to read."
32
+ )
33
+ def read(path: str, start_line: Optional[int], line_count: Optional[int]):
34
+ """Read a source file.
35
+
36
+ \b
37
+ Examples:
38
+ unity-mcp code read "Assets/Scripts/Player.cs"
39
+ unity-mcp code read "Assets/Scripts/Player.cs" --start-line 10 --line-count 20
40
+ """
41
+ config = get_config()
42
+
43
+ # Extract name and directory from path
44
+ parts = path.replace("\\", "/").split("/")
45
+ filename = os.path.splitext(parts[-1])[0]
46
+ directory = "/".join(parts[:-1]) or "Assets"
47
+
48
+ params: dict[str, Any] = {
49
+ "action": "read",
50
+ "name": filename,
51
+ "path": directory,
52
+ }
53
+
54
+ if start_line:
55
+ params["startLine"] = start_line
56
+ if line_count:
57
+ params["lineCount"] = line_count
58
+
59
+ try:
60
+ result = run_command("manage_script", params, config)
61
+ # For read, output content directly if available
62
+ if result.get("success") and result.get("data"):
63
+ data = result.get("data", {})
64
+ if isinstance(data, dict) and "contents" in data:
65
+ click.echo(data["contents"])
66
+ else:
67
+ click.echo(format_output(result, config.format))
68
+ else:
69
+ click.echo(format_output(result, config.format))
70
+ except UnityConnectionError as e:
71
+ print_error(str(e))
72
+ sys.exit(1)
73
+
74
+
75
+ @code.command("search")
76
+ @click.argument("pattern")
77
+ @click.argument("path")
78
+ @click.option(
79
+ "--max-results", "-n",
80
+ default=50,
81
+ type=int,
82
+ help="Maximum number of results (default: 50)."
83
+ )
84
+ @click.option(
85
+ "--case-sensitive", "-c",
86
+ is_flag=True,
87
+ help="Make search case-sensitive (default: case-insensitive)."
88
+ )
89
+ def search(pattern: str, path: str, max_results: int, case_sensitive: bool):
90
+ """Search for patterns in Unity scripts using regex.
91
+
92
+ PATTERN is a regex pattern to search for.
93
+ PATH is the script path (e.g., Assets/Scripts/Player.cs).
94
+
95
+ \\b
96
+ Examples:
97
+ unity-mcp code search "class.*Player" "Assets/Scripts/Player.cs"
98
+ unity-mcp code search "private.*int" "Assets/Scripts/GameManager.cs"
99
+ unity-mcp code search "TODO|FIXME" "Assets/Scripts/Utils.cs"
100
+ """
101
+ import re
102
+ import base64
103
+
104
+ config = get_config()
105
+
106
+ # Extract name and directory from path
107
+ parts = path.replace("\\", "/").split("/")
108
+ filename = os.path.splitext(parts[-1])[0]
109
+ directory = "/".join(parts[:-1]) or "Assets"
110
+
111
+ # Step 1: Read the file via Unity's manage_script
112
+ read_params: dict[str, Any] = {
113
+ "action": "read",
114
+ "name": filename,
115
+ "path": directory,
116
+ }
117
+
118
+ try:
119
+ result = run_command("manage_script", read_params, config)
120
+
121
+ # Handle nested response structure: {status, result: {success, data}}
122
+ inner_result = result.get("result", result)
123
+
124
+ if not inner_result.get("success") and result.get("status") != "success":
125
+ click.echo(format_output(result, config.format))
126
+ return
127
+
128
+ # Get file contents from nested data
129
+ data = inner_result.get("data", {})
130
+ contents = data.get("contents")
131
+
132
+ # Handle base64 encoded content
133
+ if not contents and data.get("contentsEncoded") and data.get("encodedContents"):
134
+ try:
135
+ contents = base64.b64decode(
136
+ data["encodedContents"]).decode("utf-8", "replace")
137
+ except (ValueError, TypeError):
138
+ pass
139
+
140
+ if not contents:
141
+ print_error(f"Could not read file content from {path}")
142
+ sys.exit(1)
143
+
144
+ # Step 2: Perform regex search locally
145
+ flags = re.MULTILINE
146
+ if not case_sensitive:
147
+ flags |= re.IGNORECASE
148
+
149
+ try:
150
+ regex = re.compile(pattern, flags)
151
+ except re.error as e:
152
+ print_error(f"Invalid regex pattern: {e}")
153
+ sys.exit(1)
154
+
155
+ found = list(regex.finditer(contents))
156
+
157
+ if not found:
158
+ print_info(f"No matches found for pattern: {pattern}")
159
+ return
160
+
161
+ results = []
162
+ for m in found[:max_results]:
163
+ start_idx = m.start()
164
+
165
+ # Calculate line number
166
+ line_num = contents.count('\n', 0, start_idx) + 1
167
+
168
+ # Get line content
169
+ line_start = contents.rfind('\n', 0, start_idx) + 1
170
+ line_end = contents.find('\n', start_idx)
171
+ if line_end == -1:
172
+ line_end = len(contents)
173
+
174
+ line_content = contents[line_start:line_end].strip()
175
+
176
+ results.append({
177
+ "line": line_num,
178
+ "content": line_content,
179
+ "match": m.group(0),
180
+ })
181
+
182
+ # Display results
183
+ click.echo(f"Found {len(results)} matches (total: {len(found)}):\n")
184
+ for match in results:
185
+ click.echo(f" Line {match['line']}: {match['content']}")
186
+
187
+ except UnityConnectionError as e:
188
+ print_error(str(e))
189
+ sys.exit(1)
@@ -0,0 +1,212 @@
1
+ """Component CLI commands."""
2
+
3
+ import sys
4
+ import json
5
+ import click
6
+ from typing import Optional, Any
7
+
8
+ from cli.utils.config import get_config
9
+ from cli.utils.output import format_output, print_error, print_success
10
+ from cli.utils.connection import run_command, UnityConnectionError
11
+
12
+
13
+ @click.group()
14
+ def component():
15
+ """Component operations - add, remove, modify components on GameObjects."""
16
+ pass
17
+
18
+
19
+ @component.command("add")
20
+ @click.argument("target")
21
+ @click.argument("component_type")
22
+ @click.option(
23
+ "--search-method",
24
+ type=click.Choice(["by_id", "by_name", "by_path"]),
25
+ default=None,
26
+ help="How to find the target GameObject."
27
+ )
28
+ @click.option(
29
+ "--properties", "-p",
30
+ default=None,
31
+ help='Initial properties as JSON (e.g., \'{"mass": 5.0}\').'
32
+ )
33
+ def add(target: str, component_type: str, search_method: Optional[str], properties: Optional[str]):
34
+ """Add a component to a GameObject.
35
+
36
+ \b
37
+ Examples:
38
+ unity-mcp component add "Player" Rigidbody
39
+ unity-mcp component add "-81840" BoxCollider --search-method by_id
40
+ unity-mcp component add "Enemy" Rigidbody --properties '{"mass": 5.0, "useGravity": true}'
41
+ """
42
+ config = get_config()
43
+
44
+ params: dict[str, Any] = {
45
+ "action": "add",
46
+ "target": target,
47
+ "componentType": component_type,
48
+ }
49
+
50
+ if search_method:
51
+ params["searchMethod"] = search_method
52
+ if properties:
53
+ try:
54
+ params["properties"] = json.loads(properties)
55
+ except json.JSONDecodeError as e:
56
+ print_error(f"Invalid JSON for properties: {e}")
57
+ sys.exit(1)
58
+
59
+ try:
60
+ result = run_command("manage_components", params, config)
61
+ click.echo(format_output(result, config.format))
62
+ if result.get("success"):
63
+ print_success(f"Added {component_type} to '{target}'")
64
+ except UnityConnectionError as e:
65
+ print_error(str(e))
66
+ sys.exit(1)
67
+
68
+
69
+ @component.command("remove")
70
+ @click.argument("target")
71
+ @click.argument("component_type")
72
+ @click.option(
73
+ "--search-method",
74
+ type=click.Choice(["by_id", "by_name", "by_path"]),
75
+ default=None,
76
+ help="How to find the target GameObject."
77
+ )
78
+ @click.option(
79
+ "--force", "-f",
80
+ is_flag=True,
81
+ help="Skip confirmation prompt."
82
+ )
83
+ def remove(target: str, component_type: str, search_method: Optional[str], force: bool):
84
+ """Remove a component from a GameObject.
85
+
86
+ \b
87
+ Examples:
88
+ unity-mcp component remove "Player" Rigidbody
89
+ unity-mcp component remove "-81840" BoxCollider --search-method by_id --force
90
+ """
91
+ config = get_config()
92
+
93
+ if not force:
94
+ click.confirm(f"Remove {component_type} from '{target}'?", abort=True)
95
+
96
+ params: dict[str, Any] = {
97
+ "action": "remove",
98
+ "target": target,
99
+ "componentType": component_type,
100
+ }
101
+
102
+ if search_method:
103
+ params["searchMethod"] = search_method
104
+
105
+ try:
106
+ result = run_command("manage_components", params, config)
107
+ click.echo(format_output(result, config.format))
108
+ if result.get("success"):
109
+ print_success(f"Removed {component_type} from '{target}'")
110
+ except UnityConnectionError as e:
111
+ print_error(str(e))
112
+ sys.exit(1)
113
+
114
+
115
+ @component.command("set")
116
+ @click.argument("target")
117
+ @click.argument("component_type")
118
+ @click.argument("property_name")
119
+ @click.argument("value")
120
+ @click.option(
121
+ "--search-method",
122
+ type=click.Choice(["by_id", "by_name", "by_path"]),
123
+ default=None,
124
+ help="How to find the target GameObject."
125
+ )
126
+ def set_property(target: str, component_type: str, property_name: str, value: str, search_method: Optional[str]):
127
+ """Set a single property on a component.
128
+
129
+ \b
130
+ Examples:
131
+ unity-mcp component set "Player" Rigidbody mass 5.0
132
+ unity-mcp component set "Enemy" Transform position "[0, 5, 0]"
133
+ unity-mcp component set "-81840" Light intensity 2.5 --search-method by_id
134
+ """
135
+ config = get_config()
136
+
137
+ # Try to parse value as JSON for complex types
138
+ try:
139
+ parsed_value = json.loads(value)
140
+ except json.JSONDecodeError:
141
+ # Keep as string if not valid JSON
142
+ parsed_value = value
143
+
144
+ params: dict[str, Any] = {
145
+ "action": "set_property",
146
+ "target": target,
147
+ "componentType": component_type,
148
+ "property": property_name,
149
+ "value": parsed_value,
150
+ }
151
+
152
+ if search_method:
153
+ params["searchMethod"] = search_method
154
+
155
+ try:
156
+ result = run_command("manage_components", params, config)
157
+ click.echo(format_output(result, config.format))
158
+ if result.get("success"):
159
+ print_success(f"Set {component_type}.{property_name} = {value}")
160
+ except UnityConnectionError as e:
161
+ print_error(str(e))
162
+ sys.exit(1)
163
+
164
+
165
+ @component.command("modify")
166
+ @click.argument("target")
167
+ @click.argument("component_type")
168
+ @click.option(
169
+ "--properties", "-p",
170
+ required=True,
171
+ help='Properties to set as JSON (e.g., \'{"mass": 5.0, "useGravity": false}\').'
172
+ )
173
+ @click.option(
174
+ "--search-method",
175
+ type=click.Choice(["by_id", "by_name", "by_path"]),
176
+ default=None,
177
+ help="How to find the target GameObject."
178
+ )
179
+ def modify(target: str, component_type: str, properties: str, search_method: Optional[str]):
180
+ """Set multiple properties on a component at once.
181
+
182
+ \b
183
+ Examples:
184
+ unity-mcp component modify "Player" Rigidbody --properties '{"mass": 5.0, "useGravity": false}'
185
+ unity-mcp component modify "Light" Light --properties '{"intensity": 2.0, "color": [1, 0, 0, 1]}'
186
+ """
187
+ config = get_config()
188
+
189
+ try:
190
+ props_dict = json.loads(properties)
191
+ except json.JSONDecodeError as e:
192
+ print_error(f"Invalid JSON for properties: {e}")
193
+ sys.exit(1)
194
+
195
+ params: dict[str, Any] = {
196
+ "action": "set_property",
197
+ "target": target,
198
+ "componentType": component_type,
199
+ "properties": props_dict,
200
+ }
201
+
202
+ if search_method:
203
+ params["searchMethod"] = search_method
204
+
205
+ try:
206
+ result = run_command("manage_components", params, config)
207
+ click.echo(format_output(result, config.format))
208
+ if result.get("success"):
209
+ print_success(f"Modified {component_type} on '{target}'")
210
+ except UnityConnectionError as e:
211
+ print_error(str(e))
212
+ sys.exit(1)