qmenta-sdk-lib 2.2.dev3494__tar.gz → 2.2.dev3495__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/PKG-INFO +1 -1
  2. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/pyproject.toml +1 -1
  3. qmenta_sdk_lib-2.2.dev3495/python/qmenta/sdk/tool_maker/base_ui.py +190 -0
  4. qmenta_sdk_lib-2.2.dev3495/python/qmenta/sdk/tool_maker/integration_ui.py +268 -0
  5. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/integration_workflow_ui.py +101 -156
  6. qmenta_sdk_lib-2.2.dev3495/python/qmenta/sdk/tool_maker/launch_gui.py +131 -0
  7. qmenta_sdk_lib-2.2.dev3494/python/qmenta/sdk/tool_maker/integration_ui.py +0 -415
  8. qmenta_sdk_lib-2.2.dev3494/python/qmenta/sdk/tool_maker/launch_gui.py +0 -173
  9. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/__init__.py +0 -0
  10. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/__init__.py +0 -0
  11. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/bids/__init__.py +0 -0
  12. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/bids/make_entrypoint.py +0 -0
  13. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/bids/wrapper.py +0 -0
  14. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/client.py +0 -0
  15. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/communication.py +0 -0
  16. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/context.py +0 -0
  17. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/directory_utils.py +0 -0
  18. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/executor.py +0 -0
  19. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/init.py +0 -0
  20. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/local/__init__.py +0 -0
  21. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/local/client.py +0 -0
  22. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/local/context.py +0 -0
  23. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/local/executor.py +0 -0
  24. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/local/parse_settings.py +0 -0
  25. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/log_capture.py +0 -0
  26. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/make_entrypoint.py +0 -0
  27. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/__init__.py +0 -0
  28. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/context.py +0 -0
  29. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/file_filter.py +0 -0
  30. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/inputs.py +0 -0
  31. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/make_files.py +0 -0
  32. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/modalities.py +0 -0
  33. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/outputs.py +0 -0
  34. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/run_test_docker.py +0 -0
  35. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/templates_tool_maker/Dockerfile_schema +0 -0
  36. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/templates_tool_maker/description_schema +0 -0
  37. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/templates_tool_maker/qmenta.png +0 -0
  38. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/templates_tool_maker/test_tool_schema +0 -0
  39. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/templates_tool_maker/tool_schema +0 -0
  40. {qmenta_sdk_lib-2.2.dev3494 → qmenta_sdk_lib-2.2.dev3495}/python/qmenta/sdk/tool_maker/tool_maker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qmenta-sdk-lib
3
- Version: 2.2.dev3494
3
+ Version: 2.2.dev3495
4
4
  Summary: QMENTA SDK for tool development.
5
5
  Author: QMENTA
6
6
  Maintainer: Marc Ramos
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "qmenta-sdk-lib"
3
- version = "2.2.dev3494"
3
+ version = "2.2.dev3495"
4
4
  description = "QMENTA SDK for tool development."
5
5
  authors = ["QMENTA"]
6
6
  packages = [
@@ -0,0 +1,190 @@
1
+ import os
2
+ from tkinter import BOTH, CENTER, LEFT, RIGHT, TOP, YES, X, Y, messagebox
3
+
4
+ import ttkbootstrap as ttk
5
+ from PIL import Image, ImageTk
6
+
7
+ # Constants for style
8
+ FONT_HEADER = "Helvetica 16 bold"
9
+ FONT_SUBHEADER = "Helvetica 12 bold"
10
+ PADDING_OUTER = 20
11
+ PADDING_ROW = 5
12
+
13
+
14
+ class UnifiedToolWindow:
15
+ """
16
+ A unified GUI window class that provides standardized styling
17
+ and helper methods. Scrolling is optional.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ title="Tool GUI",
23
+ geometry="600x800",
24
+ theme="darkly",
25
+ scrollable=True,
26
+ ):
27
+ self.window = ttk.Window(themename=theme)
28
+ self.window.title(title)
29
+ self.window.geometry(geometry)
30
+
31
+ # Only allow resizing if specifically needed or strictly scrollable
32
+ self.window.resizable(True, True)
33
+
34
+ self.gui_content = {}
35
+ self.scrollable = scrollable
36
+
37
+ if self.scrollable:
38
+ self._setup_scroll_area()
39
+ self._setup_scroll_bindings()
40
+ else:
41
+ self._setup_static_area()
42
+
43
+ def _setup_scroll_area(self):
44
+ """Sets up the complex canvas and scrollbar structure."""
45
+ self.main_container = ttk.Frame(self.window)
46
+ self.main_container.pack(fill=BOTH, expand=YES)
47
+
48
+ self.canvas = ttk.Canvas(self.main_container)
49
+ self.vbar = ttk.Scrollbar(
50
+ self.main_container, orient="vertical", command=self.canvas.yview
51
+ )
52
+ self.canvas.configure(yscrollcommand=self.vbar.set)
53
+
54
+ self.vbar.pack(side=RIGHT, fill=Y)
55
+ self.canvas.pack(side=LEFT, fill=BOTH, expand=YES)
56
+
57
+ self.content_frame = ttk.Frame(self.canvas, padding=PADDING_OUTER)
58
+ self.canvas_window_id = self.canvas.create_window(
59
+ (0, 0), window=self.content_frame, anchor="nw", tags="inner_frame"
60
+ )
61
+
62
+ def _setup_static_area(self):
63
+ """Sets up a simple frame without scrolling."""
64
+ self.content_frame = ttk.Frame(self.window, padding=PADDING_OUTER)
65
+ self.content_frame.pack(fill=BOTH, expand=YES)
66
+
67
+ def _setup_scroll_bindings(self):
68
+ """
69
+ Sets up resize events and mousewheel scrolling
70
+ Only for scrollable windows
71
+ """
72
+ self.content_frame.bind(
73
+ "<Configure>",
74
+ lambda e: self.canvas.configure(
75
+ scrollregion=self.canvas.bbox("all")
76
+ ),
77
+ )
78
+ self.canvas.bind(
79
+ "<Configure>",
80
+ lambda e: self.canvas.itemconfig(
81
+ self.canvas_window_id, width=e.width
82
+ ),
83
+ )
84
+
85
+ def _on_mousewheel(event):
86
+ if event.delta: # Windows/macOS
87
+ self.canvas.yview_scroll(
88
+ int(-1 * (event.delta / 120)), "units"
89
+ )
90
+ elif event.num == 4: # Linux Up
91
+ self.canvas.yview_scroll(-1, "units")
92
+ elif event.num == 5: # Linux Down
93
+ self.canvas.yview_scroll(1, "units")
94
+
95
+ self.window.bind_all("<MouseWheel>", _on_mousewheel)
96
+ self.window.bind_all("<Button-4>", _on_mousewheel)
97
+ self.window.bind_all("<Button-5>", _on_mousewheel)
98
+
99
+ # --- Widget Helpers ---
100
+
101
+ def add_logo(self, relative_path="templates_tool_maker/qmenta.png"):
102
+ try:
103
+ img_path = os.path.join(os.path.dirname(__file__), relative_path)
104
+ if os.path.exists(img_path):
105
+ logo = Image.open(img_path).resize((500, 110))
106
+ img = ImageTk.PhotoImage(logo)
107
+ lbl = ttk.Label(
108
+ master=self.content_frame, image=img, anchor=CENTER
109
+ )
110
+ lbl.image = img
111
+ lbl.pack(side=TOP, pady=(0, 20))
112
+ except Exception as e:
113
+ print(f"Image load failed: {e}")
114
+
115
+ def add_header(self, text, subtext=None, warning_text=None):
116
+ ttk.Label(self.content_frame, text=text, font=FONT_HEADER).pack(
117
+ pady=10
118
+ )
119
+ if subtext:
120
+ ttk.Label(self.content_frame, text=subtext, bootstyle="info").pack(
121
+ pady=(0, 5)
122
+ )
123
+ if warning_text:
124
+ ttk.Label(
125
+ self.content_frame, text=warning_text, bootstyle="warning"
126
+ ).pack(pady=(0, 20))
127
+
128
+ def add_section_label(self, text):
129
+ ttk.Label(self.content_frame, text=text, font=FONT_SUBHEADER).pack(
130
+ pady=10
131
+ )
132
+
133
+ def create_row(self, label_text, is_password=False, default=None):
134
+ frame = ttk.Frame(master=self.content_frame)
135
+ frame.pack(fill=X, pady=PADDING_ROW)
136
+
137
+ lbl = ttk.Label(master=frame, text=label_text, width=30)
138
+ lbl.pack(side=LEFT)
139
+
140
+ entry = ttk.Entry(master=frame, show="*" if is_password else None)
141
+ if default:
142
+ entry.insert(0, str(default))
143
+ entry.pack(side=LEFT, fill=X, expand=YES, padx=10)
144
+ return entry
145
+
146
+ def add_separator(self):
147
+ ttk.Separator(self.content_frame, orient="horizontal").pack(
148
+ fill=X, pady=20
149
+ )
150
+
151
+ def add_action_buttons(
152
+ self, submit_command, quit_command=None, submit_text="Publish"
153
+ ):
154
+ action_frame = ttk.Frame(master=self.content_frame)
155
+ action_frame.pack(pady=20)
156
+
157
+ def wrapped_submit():
158
+ if submit_command():
159
+ self.window.destroy()
160
+
161
+ def wrapped_quit():
162
+ if quit_command:
163
+ quit_command()
164
+ self.window.destroy()
165
+ exit()
166
+
167
+ ttk.Button(
168
+ master=action_frame,
169
+ text=submit_text,
170
+ command=wrapped_submit,
171
+ bootstyle="primary",
172
+ ).pack(side=LEFT, padx=10)
173
+
174
+ ttk.Button(
175
+ master=action_frame,
176
+ text="Quit",
177
+ command=wrapped_quit,
178
+ bootstyle="danger",
179
+ ).pack(side=LEFT, padx=10)
180
+
181
+ def run(self):
182
+ self.window.mainloop()
183
+
184
+ @staticmethod
185
+ def show_error(title, message):
186
+ messagebox.showerror(title, message)
187
+
188
+ @staticmethod
189
+ def show_info(title, message):
190
+ messagebox.showinfo(title, message)
@@ -0,0 +1,268 @@
1
+ #! /usr/bin/env python
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from tkinter import END, LEFT, filedialog
7
+
8
+ import ttkbootstrap as ttk
9
+ from qmenta.core import platform
10
+ from qmenta.core.auth import Needs2FAError
11
+ from qmenta.sdk.tool_maker.make_files import raise_if_false
12
+
13
+ # Import shared UI
14
+ from base_ui import UnifiedToolWindow
15
+
16
+ MIN = 0
17
+ MAX_CORES = 10
18
+ MAX_RAM = 16
19
+
20
+ dir_name = None
21
+
22
+
23
+ def gui_tkinter():
24
+ # Initialize the Shared Window
25
+ gui = UnifiedToolWindow(title="Tool Publishing GUI", scrollable=True)
26
+
27
+ # Logo & Headers
28
+ gui.add_logo()
29
+ gui.add_header(
30
+ "QMENTA Platform credentials",
31
+ warning_text="(*) indicates mandatory field.",
32
+ )
33
+
34
+ # Credential Fields
35
+ user_id_entry = gui.create_row("Username*")
36
+ password_entry = gui.create_row("Password*", is_password=True)
37
+
38
+ # Custom File Browse Row (Logic specific to this script)
39
+ write_dir = ttk.StringVar()
40
+
41
+ def browse_folder():
42
+ global dir_name
43
+ dir_name = filedialog.askdirectory()
44
+ write_dir.set(dir_name)
45
+ tool_id_entry.delete(0, END)
46
+ tool_id_entry.insert(0, os.path.basename(dir_name))
47
+
48
+ # checking version
49
+ version_file = os.path.join(dir_name, "version")
50
+ if os.path.exists(version_file):
51
+ with open(version_file) as fv:
52
+ version_ = fv.read().strip()
53
+ tool_version_entry.delete(0, END)
54
+ tool_version_entry.insert(0, version_)
55
+ image_name_entry.delete(0, END)
56
+ image_name_entry.insert(
57
+ 0, os.path.basename(dir_name) + f":{version_}"
58
+ )
59
+
60
+ folder_frame = ttk.Frame(master=gui.content_frame)
61
+ folder_frame.pack(fill="x", pady=10)
62
+ ttk.Label(folder_frame, text="Select tool folder*", width=30).pack(
63
+ side=LEFT
64
+ )
65
+ ttk.Button(folder_frame, text="Browse Folder", command=browse_folder).pack(
66
+ side=LEFT, padx=10
67
+ )
68
+ ttk.Label(folder_frame, textvariable=write_dir).pack(side=LEFT, padx=10)
69
+
70
+ # Tool Info Fields
71
+ tool_id_entry = gui.create_row("Specify the tool ID*")
72
+ tool_version_entry = gui.create_row("Specify the tool version*")
73
+ tool_name_entry = gui.create_row("Specify the tool name*")
74
+ num_cores_entry = gui.create_row(
75
+ f"Cores required (integer, max {MAX_CORES})", default="1"
76
+ )
77
+ memory_entry = gui.create_row(
78
+ f"RAM required GB (integer, max {MAX_RAM})", default="1"
79
+ )
80
+ image_name_entry = gui.create_row("Docker image name")
81
+
82
+ gui.add_separator()
83
+
84
+ # Docker Registry Fields
85
+ gui.add_section_label("Docker Registry Credentials")
86
+ docker_url_entry = gui.create_row(
87
+ "Docker registry URL", default="hub.docker.com"
88
+ )
89
+ docker_user_entry = gui.create_row("Docker registry user")
90
+ docker_password_entry = gui.create_row(
91
+ "Docker registry password*", is_password=True
92
+ )
93
+
94
+ # --- Logic ---
95
+ gui_content = {}
96
+
97
+ def generate_output_object():
98
+ return {
99
+ "qmenta_user": user_id_entry.get(),
100
+ "qmenta_password": password_entry.get(),
101
+ "code": tool_id_entry.get(),
102
+ "version": tool_version_entry.get(),
103
+ "name": tool_name_entry.get(),
104
+ "cores": num_cores_entry.get(),
105
+ "memory": memory_entry.get(),
106
+ "image_name": image_name_entry.get(),
107
+ "docker_url": docker_url_entry.get(),
108
+ "docker_user": docker_user_entry.get(),
109
+ "docker_password": docker_password_entry.get(),
110
+ }
111
+
112
+ def perform_selection_check(values):
113
+ try:
114
+ raise_if_false(
115
+ isinstance(values["code"], str), "Tool ID must be a string."
116
+ )
117
+ raise_if_false(values["code"] != "", "Tool ID must be defined.")
118
+ raise_if_false(
119
+ " " not in values["code"], "Tool ID can't have spaces."
120
+ )
121
+ values["code"] = values["code"].lower()
122
+ values["short_name"] = values["code"].lower().replace(" ", "_")
123
+
124
+ raise_if_false(
125
+ isinstance(values["name"], str), "Tool name must be a string."
126
+ )
127
+ raise_if_false(values["name"] != "", "Tool name must be defined.")
128
+ raise_if_false(
129
+ values["version"] != "", "Tool version must be defined."
130
+ )
131
+ raise_if_false(
132
+ re.search(r"^(\d+\.)?(\d+\.)?(\*|\d+)$", values["version"]),
133
+ "Version format not valid.",
134
+ )
135
+
136
+ raise_if_false(values["cores"], "Number of cores must be defined.")
137
+ raise_if_false(
138
+ values["cores"].isnumeric(),
139
+ "Number of cores must be an integer.",
140
+ )
141
+ raise_if_false(
142
+ MAX_CORES >= int(values["cores"]) > MIN,
143
+ f"Number of cores must be between {MIN} and {MAX_CORES}.",
144
+ )
145
+
146
+ raise_if_false(values["memory"], "RAM must be defined.")
147
+ raise_if_false(
148
+ values["memory"].isnumeric(), "RAM must be an integer."
149
+ )
150
+ raise_if_false(
151
+ MAX_RAM >= int(values["memory"]) > MIN,
152
+ f"RAM must be {MIN} and {MAX_RAM}.",
153
+ )
154
+
155
+ values["memory"] = int(values["memory"])
156
+ values["image_name"] = (
157
+ values["image_name"]
158
+ or values["code"] + ":" + values["version"]
159
+ )
160
+ return values
161
+ except AssertionError as e:
162
+ gui.show_error("Error", f"AN EXCEPTION OCCURRED! {e}")
163
+ return False
164
+
165
+ def submit_form():
166
+ gui_content.update(generate_output_object())
167
+ res = perform_selection_check(gui_content)
168
+ if res:
169
+ gui_content.update(res)
170
+ return True # Signal to close window
171
+ return False
172
+
173
+ # Add Buttons & Run
174
+ gui.add_action_buttons(
175
+ submit_form, submit_text="Publish in QMENTA Platform"
176
+ )
177
+ gui.run()
178
+
179
+ return gui_content
180
+
181
+
182
+ def main():
183
+ content_build = gui_tkinter()
184
+ if not content_build:
185
+ exit()
186
+ # Ensure dir_name was set by browse_folder
187
+ if dir_name:
188
+ os.chdir(dir_name)
189
+
190
+ os.chdir(dir_name)
191
+
192
+ user = content_build["qmenta_user"]
193
+ password = content_build["qmenta_password"]
194
+ try:
195
+ auth = platform.Auth.login(
196
+ username=user,
197
+ password=password,
198
+ base_url="https://platform.qmenta.com",
199
+ ask_for_2fa_input=False,
200
+ )
201
+ except Needs2FAError as needs2faerror:
202
+ gui_tkinter.show_info(
203
+ "Message",
204
+ str(needs2faerror)
205
+ + " Please check the terminal to add the code sent to your phone.",
206
+ )
207
+ auth = platform.Auth.login(
208
+ username=user,
209
+ password=password,
210
+ base_url="https://platform.qmenta.com",
211
+ code_2fa=input("Input 2-FA code:"),
212
+ )
213
+
214
+ # Get information from the advanced options file.
215
+ advanced_options = "settings.json"
216
+ raise_if_false(
217
+ os.path.exists(advanced_options),
218
+ "Settings do not exist! Run the local test to create it.",
219
+ )
220
+ with open(advanced_options) as fr:
221
+ content_build["advanced_options"] = fr.read()
222
+
223
+ # Get information from the description file.
224
+ with open("description.html") as fr:
225
+ content_build["description"] = fr.read()
226
+
227
+ with open("results_configuration.json", "r") as file:
228
+ results_config = json.load(file)
229
+ # The screen value is expected as a string with escaped chars (dict)
230
+ results_config["screen"] = json.dumps(results_config["screen"])
231
+ content_build["results_configuration"] = json.dumps(
232
+ results_config
233
+ ).replace("{}", "")
234
+
235
+ content_build.update(
236
+ {
237
+ "start_condition_code": "output={'OK': True, 'code': 1}",
238
+ "entry_point": "/root/entrypoint.sh",
239
+ "tool_path": "tool:run",
240
+ }
241
+ )
242
+
243
+ # After creating the workflow, the ID of the workflow must be requested
244
+ # and added to the previous dictionary
245
+ # otherwise it will keep creating new workflows on the platform
246
+ # creating conflicts.
247
+ res = platform.post(
248
+ auth, "analysis_manager/upsert_user_tool", data=content_build
249
+ )
250
+
251
+ if res.json()["success"] == 1:
252
+ print("Tool updated successfully!")
253
+ print(
254
+ "Tool name:",
255
+ content_build["name"],
256
+ "(",
257
+ content_build["code"],
258
+ ":",
259
+ content_build["version"],
260
+ ")",
261
+ )
262
+ else:
263
+ print("ERROR setting the tool.")
264
+ print(res.json())
265
+
266
+
267
+ if __name__ == "__main__":
268
+ main()