pywebexec 2.0.17__py3-none-any.whl → 2.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.
pywebexec/pywebexec.py CHANGED
@@ -586,7 +586,15 @@ def read_commands():
586
586
  command_id = filename[:-5]
587
587
  status = read_command_status(command_id)
588
588
  if status:
589
- command = command_str(status.get('command', '-'), status.get('params', []))
589
+ cmd = status.get('command', '-')
590
+ params = status.get('params', [])
591
+ if cmd.endswith('/run-para'):
592
+ cmd = "batch"
593
+ index = params.index('run')
594
+ if index:
595
+ params = params[index+1:]
596
+ params[0] = os.path.basename(params[0])
597
+ command = command_str(cmd, params)
590
598
  if status.get('status') == 'running' and status.get('last_update',0)<datetime.now().timestamp()-5:
591
599
  output_file_path = get_output_file_path(command_id)
592
600
  if os.path.exists(output_file_path):
@@ -827,6 +835,8 @@ def run_dynamic_command(cmd):
827
835
  noprefix = exe.get("schema", {}).get("schema_options", {}).get("noprefix_params", {})
828
836
  convert_params = exe.get("schema", {}).get("schema_options", {}).get("convert_params", {})
829
837
  schema_params = exe.get("schema", {}).get("properties", {})
838
+ batch_param = exe.get("schema", {}).get("schema_options", {}).get("batch_param", None)
839
+ batch_values = []
830
840
  if isinstance(data_params, dict) and not schema_params:
831
841
  return None
832
842
  if isinstance(data_params, dict):
@@ -852,6 +862,10 @@ def run_dynamic_command(cmd):
852
862
  params += f"{prefix} "
853
863
  continue
854
864
  params += f"{prefix}{separator}"
865
+ values = shlex.split(value) if isinstance(value, str) else value
866
+ if param == batch_param and len(values)>1:
867
+ batch_values = values
868
+ value="@1"
855
869
  if isinstance(value, list):
856
870
  params += " ".join(value)
857
871
  else:
@@ -865,7 +879,7 @@ def run_dynamic_command(cmd):
865
879
  params = shlex.split(' '.join(params)) if isinstance(params, list) else []
866
880
  except Exception as e:
867
881
  return None
868
- return params
882
+ return params, batch_values
869
883
 
870
884
  cmd_path = os.path.join(".", os.path.basename(cmd))
871
885
  if not os.path.isfile(cmd_path) or not os.access(cmd_path, os.X_OK):
@@ -874,11 +888,13 @@ def run_dynamic_command(cmd):
874
888
  data = request.json
875
889
  except Exception as e:
876
890
  data = {}
877
- params = parse_params(cmd, data.get('params'))
891
+ params, batch_values = parse_params(cmd, data.get('params'))
878
892
  if params is None:
879
893
  return jsonify({'error': 'Invalid parameters'}), 400
880
894
  rows = data.get('rows', tty_rows) or tty_rows
881
895
  cols = data.get('cols', tty_cols) or tty_cols
896
+ parallel = data.get('parallel', 1)
897
+ delay = data.get('delay', 0)
882
898
  user = session.get('username', '-')
883
899
  command_id = str(uuid.uuid4())
884
900
  update_command_status(command_id, {
@@ -889,6 +905,10 @@ def run_dynamic_command(cmd):
889
905
  'from': request.remote_addr,
890
906
  })
891
907
  Path(get_output_file_path(command_id)).touch()
908
+ if batch_values:
909
+ params = ["-n", "-p", str(parallel), "-d", str(delay), "-P", *batch_values, '--', sys.argv[0], "-d", ".", "--", "run", cmd_path, *params]
910
+ cmd_path = shutil.which("run-para")
911
+ print(params)
892
912
  thread = threading.Thread(target=run_command, args=(request.remote_addr, user, cmd_path, params, command_id, rows, cols))
893
913
  thread.start()
894
914
  return jsonify({'message': 'Command is running', 'command_id': command_id})
@@ -1021,13 +1041,16 @@ def swagger_yaml():
1021
1041
  "properties": {
1022
1042
  "params": {"type": "array", "items": {"type": "string"}, "default": []},
1023
1043
  "rows": {"type": "integer", "description": "tty nb rows", "default": tty_rows},
1024
- "cols": {"type": "integer", "description": "tty nb columns", "default": tty_cols}
1044
+ "cols": {"type": "integer", "description": "tty nb columns", "default": tty_cols},
1025
1045
  }
1026
1046
  }
1027
1047
  if exe["schema"]:
1028
- # if exec["schema"].get("schema_options"):
1029
- # del exe["schema"]["schema_options"]
1030
1048
  cmd_schema["properties"]["params"] = exe["schema"]
1049
+ if exe["schema"].get("schema_options", {}).get("batch_param"):
1050
+ cmd_schema["properties"].update({
1051
+ "parallel": {"type": "integer", "description": 'nb parallel jobs', "default": 1, "required": True, "minimum": 1, "maximum": 100},
1052
+ "delay": {"type": "number", "description": "initial delay in s between jobs", "default": 10, "required": True, "minimum": 0, "maximum": 600},
1053
+ })
1031
1054
  swagger_spec.setdefault("paths", {})[dynamic_path] = {
1032
1055
  "post": {
1033
1056
  "summary": f"Run command {exe['command']}",
@@ -1038,7 +1061,7 @@ def swagger_yaml():
1038
1061
  "parameters": [
1039
1062
  {
1040
1063
  "in": "body",
1041
- "name": "reuqestBody",
1064
+ "name": "requestBody",
1042
1065
  "schema": cmd_schema
1043
1066
  }
1044
1067
  ],
@@ -1062,6 +1085,40 @@ def swagger_yaml():
1062
1085
  except Exception as e:
1063
1086
  return Response(f"Error reading swagger spec: {e}", status=500)
1064
1087
 
1088
+ @app.route('/commands/<command_id>/run', methods=['POST'])
1089
+ def relaunch_command(command_id):
1090
+ log_request(f"relaunch_command {command_id}")
1091
+ status = read_command_status(command_id)
1092
+ if not status:
1093
+ return jsonify({'error': 'Invalid command_id'}), 404
1094
+
1095
+ data = request.json or {}
1096
+ command = status['command']
1097
+ params = status['params']
1098
+ rows = data.get('rows', tty_rows)
1099
+ cols = data.get('cols', tty_cols)
1100
+
1101
+ command_path = command
1102
+ if not os.path.isfile(command_path) or not os.access(command_path, os.X_OK):
1103
+ return jsonify({'error': 'Command not found or not executable'}), 400
1104
+
1105
+ # Get the user from the session
1106
+ user = session.get('username', '-')
1107
+ new_command_id = str(uuid.uuid4())
1108
+ update_command_status(new_command_id, {
1109
+ 'status': 'running',
1110
+ 'command': command,
1111
+ 'params': params,
1112
+ 'user': user,
1113
+ 'from': request.remote_addr,
1114
+ })
1115
+
1116
+ Path(get_output_file_path(new_command_id)).touch()
1117
+ thread = threading.Thread(target=run_command, args=(request.remote_addr, user, command_path, params, new_command_id, rows, cols))
1118
+ thread.start()
1119
+
1120
+ return jsonify({'message': 'Command is running', 'command_id': new_command_id})
1121
+
1065
1122
  def main():
1066
1123
  global COMMAND_STATUS_DIR
1067
1124
  basef = f"{CONFDIR}/pywebexec_{args.listen}:{args.port}"
@@ -6,8 +6,8 @@
6
6
  }
7
7
 
8
8
  #schemaForm, .schema-form {
9
- font-size: 14px;
10
-
9
+ font-size: 14px;
10
+
11
11
  .glyphicon-plus-sign:before {
12
12
  content: "\e081";
13
13
  }
@@ -142,6 +142,7 @@ font-size: 14px;
142
142
  min-width: unset;
143
143
  max-width: unset;
144
144
  width: auto;
145
+ margin: 0;
145
146
  }
146
147
  input[type="number"] {
147
148
  max-width: 120px;
@@ -161,7 +162,7 @@ font-size: 14px;
161
162
  border-radius: 15px;
162
163
  border-bottom-right-radius: 5px;
163
164
  border: 1px solid #ccc;
164
- padding: 3px 10px;
165
+ padding: 2px 10px 0px 10px;
165
166
  margin: 0px;
166
167
  width: unset;
167
168
  line-height: 1.42857143;
@@ -263,8 +263,7 @@ paramsInput.addEventListener('focus', () => {
263
263
  if (paramsContainer.style.display == 'none') {
264
264
  $('#schemaForm').html('');
265
265
  }
266
- if (gExecutables[currentCmd] && gExecutables[currentCmd].schema && paramsContainer.style.display == 'none') {
267
- // sProp = gExecutables[currentCmd].schema.properties;
266
+ if (gExecutables[currentCmd] && gExecutables[currentCmd].schema && gExecutables[currentCmd].schema.properties && paramsContainer.style.display == 'none') {
268
267
  createSchemaForm($('#schemaForm'), gExecutables[currentCmd].schema, async function (errors, values) {
269
268
  if (errors) {
270
269
  console.log(errors);
@@ -272,13 +271,20 @@ paramsInput.addEventListener('focus', () => {
272
271
  const commandName = commandInput.value;
273
272
  fitAddon.fit();
274
273
  terminal.clear();
274
+ payload = { params: values, rows: terminal.rows, cols: terminal.cols }
275
+ if ('parallel' in values) {
276
+ payload['parallel'] = values['parallel'];
277
+ payload['delay'] = values['delay'];
278
+ delete payload['params']['parallel'];
279
+ delete payload['params']['delay'];
280
+ }
275
281
  try {
276
282
  const response = await fetch(`/commands/${commandName}`, {
277
283
  method: 'POST',
278
284
  headers: {
279
285
  'Content-Type': 'application/json'
280
286
  },
281
- body: JSON.stringify({ params: values, rows: terminal.rows, cols: terminal.cols })
287
+ body: JSON.stringify(payload)
282
288
  });
283
289
 
284
290
  if (!response.ok) {
@@ -11,11 +11,16 @@ function adjustInputWidth(input) {
11
11
  function formInputHandle() {
12
12
  schemaForm.querySelectorAll('input[type="text"], input[type="number"]').forEach(input => {
13
13
  if (! inputHandlers.includes(input)) {
14
- if (input.value) {
15
- input.setAttribute('size', Math.max(input.value.length - 2, 2));
14
+ val = input.value || input.placeholder;
15
+ if (val) {
16
+ size = Math.max(val.length - 2, 2)
17
+ if (input.type== 'number') {
18
+ size += 2;
19
+ }
16
20
  } else {
17
- input.setAttribute('size', '12');
21
+ size = 12;
18
22
  }
23
+ input.setAttribute('size', size);
19
24
  input.addEventListener('input', () => adjustInputWidth(input));
20
25
  inputHandlers.push(input);
21
26
  }
@@ -42,10 +47,68 @@ function createSchemaForm(form, schema, onSubmit) {
42
47
  formDesc = extractKeysAndPlaceholders(schema);
43
48
  schemaForm = form[0];
44
49
  if (onSubmit != null) {
50
+ console.log(schema.schema_options.batch_param)
51
+ if (schema && schema.schema_options && schema.schema_options.batch_param) {
52
+ if (!schema.properties.parallel) {
53
+ schema.properties['parallel'] = {
54
+ type: 'integer',
55
+ default: 1,
56
+ minimum: 1,
57
+ maximum: 100,
58
+ required: true,
59
+ description: "nb parallel jobs"
60
+ };
61
+ schema.properties['delay'] = {
62
+ type: 'integer',
63
+ default: 10,
64
+ minimum: 0,
65
+ maximum: 600,
66
+ required: true,
67
+ description: "initial delay in s between jobs"
68
+ };
69
+ formDesc.push({
70
+ key: 'parallel',
71
+ });
72
+ formDesc.push({
73
+ key: 'delay',
74
+ });
75
+ }
76
+ for (i = 0; i < formDesc.length; i++) {
77
+ if (formDesc[i].key == schema.schema_options.batch_param) {
78
+ formDesc[i].type = 'textarea';
79
+ formDesc[i].required = true;
80
+ }
81
+ if (formDesc[i].key == 'parallel') {
82
+ formDesc[i].type = 'range';
83
+ formDesc[i].indicator = true;
84
+ }
85
+ if (formDesc[i].key == 'delay') {
86
+ formDesc[i].type = 'range';
87
+ formDesc[i].indicator = true;
88
+ }
89
+ }
90
+ }
45
91
  formDesc.push({
46
92
  type: 'submit',
47
93
  title: 'Run',
48
94
  });
95
+ } else {
96
+ if (schema && schema.properties && schema.properties.params.schema_options && schema.properties.params.schema_options.batch_param) {
97
+ for (i = 0; i < formDesc.length; i++) {
98
+ if (formDesc[i].key == 'params.' + schema.properties.params.schema_options.batch_param) {
99
+ formDesc[i].type = 'textarea';
100
+ formDesc[i].required = true;
101
+ }
102
+ if (formDesc[i].key == 'parallel') {
103
+ formDesc[i].type = 'range';
104
+ formDesc[i].indicator = true;
105
+ }
106
+ if (formDesc[i].key == 'delay') {
107
+ formDesc[i].type = 'range';
108
+ formDesc[i].indicator = true;
109
+ }
110
+ }
111
+ }
49
112
  }
50
113
  form[0].classList.add('form-inline');
51
114
  jsform = form.jsonForm({
@@ -293,27 +293,16 @@ async function relaunchCommand(command_id, event) {
293
293
  event.stopPropagation();
294
294
  event.stopImmediatePropagation();
295
295
  try {
296
- const response = await fetch(`/commands/${command_id}`);
297
- if (!response.ok) {
298
- throw new Error('Failed to fetch command status');
299
- }
300
- const data = await response.json();
301
- if (data.error) {
302
- alert(data.error);
303
- return;
304
- }
305
296
  fitAddon.fit();
306
297
  terminal.clear();
307
- const relaunchResponse = await fetch(`/commands`, {
298
+ const relaunchResponse = await fetch(`/commands/${command_id}/run`, {
308
299
  method: 'POST',
309
300
  headers: {
310
301
  'Content-Type': 'application/json'
311
302
  },
312
303
  body: JSON.stringify({
313
- command: data.command,
314
- params: data.params,
315
304
  rows: terminal.rows,
316
- cols: terminal.cols,
305
+ cols: terminal.cols
317
306
  })
318
307
  });
319
308
  if (!relaunchResponse.ok) {
@@ -69,8 +69,8 @@ window.onload = function() {
69
69
  const observer = new MutationObserver((mutations) => {
70
70
  mutations.forEach(mutation => {
71
71
  mutation.addedNodes.forEach(node => {
72
- if (node.classList.contains("highlight-code") ||
73
- node.classList.contains("body-param__text")) {
72
+ if (node.classList && (node.classList.contains("highlight-code") ||
73
+ node.classList.contains("body-param__text"))) {
74
74
  // Retrieve the data-path attribute from the first opblock-summary-path element
75
75
  const routePath = $(node).closest('.opblock').find('.opblock-summary-path').first().attr('data-path');
76
76
  const routePathId = `schemaForm${routePath.replaceAll("/", "_")}`;
pywebexec/swagger.yaml CHANGED
@@ -194,6 +194,39 @@ paths:
194
194
  properties:
195
195
  message:
196
196
  type: string
197
+ /commands/{command_id}/run:
198
+ post:
199
+ summary: "Relaunch a command"
200
+ tags:
201
+ - commands
202
+ parameters:
203
+ - in: path
204
+ name: command_id
205
+ required: true
206
+ type: string
207
+ - in: body
208
+ name: requestBody
209
+ schema:
210
+ type: object
211
+ properties:
212
+ rows:
213
+ type: integer
214
+ description: "tty nb rows"
215
+ default: 24
216
+ cols:
217
+ type: integer
218
+ description: "tty nb columns"
219
+ default: 125
220
+ responses:
221
+ "200":
222
+ description: "Command relaunched"
223
+ schema:
224
+ type: object
225
+ properties:
226
+ message:
227
+ type: string
228
+ command_id:
229
+ type: string
197
230
  /commands/exposed:
198
231
  get:
199
232
  summary: "List available executable commands"
pywebexec/version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.0.17'
21
- __version_tuple__ = version_tuple = (2, 0, 17)
20
+ __version__ = version = '2.1.0'
21
+ __version_tuple__ = version_tuple = (2, 1, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 2.0.17
3
+ Version: 2.1.0
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -1,9 +1,9 @@
1
1
  pywebexec/__init__.py,sha256=197fHJy0UDBwTTpGCGortZRr-w2kTaD7MxqdbVmTEi0,61
2
2
  pywebexec/host_ip.py,sha256=Ud_HTflWVQ8789aoQ2RZdT1wGI-ccvrwSWGz_c7T3TI,1241
3
- pywebexec/pywebexec.py,sha256=z_ri7z0r2jrpf1yeih1TEFaBnH39y40zfmuZY2UIu5c,42061
4
- pywebexec/swagger.yaml,sha256=j-KZnU4rGR-Fsk8y_EbqYzqVgtAZ8EjZIKAqZSxyyc0,5858
5
- pywebexec/version.py,sha256=DMmD4ZGa2_6FkPEauCreBHTWcl7y2bBceqfptG6Zlmw,513
6
- pywebexec/static/css/form.css,sha256=Zek0sCsO6CEP5fqss2C8P-OLg9FysP8r-90xwxwSWRk,4471
3
+ pywebexec/pywebexec.py,sha256=pffAT2SEE4XVXng3n_dyFv9q0rslP829u3KwEtQ3Kbo,44634
4
+ pywebexec/swagger.yaml,sha256=zP_Nz69vZx0iwbKTwiQgSs8rJRUTiGKRyIkWzMPANOE,6688
5
+ pywebexec/version.py,sha256=dseuoOPG9WZ1Ezr1SC3wS9_hczkX-b1NdE4TQPHFJso,511
6
+ pywebexec/static/css/form.css,sha256=EpYUaBt2wLYX-Vi4gq6ZFzR2LLu7bd2patNWe-BAhDs,4499
7
7
  pywebexec/static/css/markdown.css,sha256=3RzUnpVBdF6cQuB_NXV7hMTc0quYU8sfyuZcpsREj6A,1939
8
8
  pywebexec/static/css/style.css,sha256=ynccbEDzK07rurLm-UirUs5j_hVfXlIgaHeUIq9WvA0,9969
9
9
  pywebexec/static/css/swagger-ui.css,sha256=xhXN8fnUaIACGHuPIEIr9-qmyYr6Zx0k2wv4Qy7Bg1Y,154985
@@ -33,11 +33,11 @@ pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65Eiuw
33
33
  pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
34
34
  pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
35
35
  pywebexec/static/images/swagger-ui.svg,sha256=FR0yeOVwe4zCYKZAjCGcT_m0Mf25NexIVaSXifIkoU0,2117
36
- pywebexec/static/js/executables.js,sha256=3HfOsrrurtnVd-AAHpkAtvnbJDB8k7nxRwtZXRQTzuI,11767
36
+ pywebexec/static/js/executables.js,sha256=Hbcpv1IZfxC406qhwkwoTvJh8TZD7kEc651IXkQDYws,12077
37
37
  pywebexec/static/js/popup.js,sha256=0fr3pp4j9D2fXEVnHyQrx2bPWFHfgbb336dbewgH1d8,9023
38
- pywebexec/static/js/schemaform.js,sha256=RUUnReUKlB6GnlhN-sI0AE4-nHWBONfqxo0owSSDED8,3095
39
- pywebexec/static/js/script.js,sha256=fwTo9Iz4xsl6wGs-Nm8-Ng8aXzDfvews5cHfzhSfg_0,17987
40
- pywebexec/static/js/swagger-form.js,sha256=uTRGKrcyBqq910RLmdlV-HpFri3RaAXpqUPSQSyoe6o,3806
38
+ pywebexec/static/js/schemaform.js,sha256=tFWI4AQPN-RwuSo5RdHBO7AcsHEt-YKcXKFmMT6FSQ8,5076
39
+ pywebexec/static/js/script.js,sha256=-CfZRzjusxRCY7Kol_KNOq8BdRdHYTc5X3vXNChlsdQ,17631
40
+ pywebexec/static/js/swagger-form.js,sha256=BU64XxyfrWx4DnpAuaWtG-9YpcHF3OzzAYzjiSH0NGo,3826
41
41
  pywebexec/static/js/js-yaml/LICENSE,sha256=oHvCRGi5ZUznalR9R6LbKC0HcztxXbTHOpi9Y5YflVA,1084
42
42
  pywebexec/static/js/js-yaml/js-yaml.min.js,sha256=Rdw90D3AegZwWiwpibjH9wkBPwS9U4bjJ51ORH8H69c,39430
43
43
  pywebexec/static/js/marked/LICENSE.md,sha256=jjo_gvWaYJWPVsoI9EVkfDKkcz3HymwsRvbriYRxq5w,2942
@@ -66,9 +66,9 @@ pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
66
66
  pywebexec/templates/index.html,sha256=KeNLBH9PN_ZuGhzwrjvRTj2oBtbINv_SbwGQpOahNWo,3820
67
67
  pywebexec/templates/popup.html,sha256=3kpMccKD_OLLhJ4Y9KRw6Ny8wQWjVaRrUfV9y5-bDiQ,1580
68
68
  pywebexec/templates/swagger_ui.html,sha256=9ngyldkyEdLonBjl97mbIZUlVk-jxwcHrvFzMSrveyU,1067
69
- pywebexec-2.0.17.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
70
- pywebexec-2.0.17.dist-info/METADATA,sha256=MQnEQHNeT4QTx7kM8o_kceb6sCUdqVKSwH2qvrxqBtY,11645
71
- pywebexec-2.0.17.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
72
- pywebexec-2.0.17.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
73
- pywebexec-2.0.17.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
74
- pywebexec-2.0.17.dist-info/RECORD,,
69
+ pywebexec-2.1.0.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
70
+ pywebexec-2.1.0.dist-info/METADATA,sha256=wYgWeQ99N96P1HwnibF89u-qjwXI-J14KJeivoh3Bbc,11644
71
+ pywebexec-2.1.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
72
+ pywebexec-2.1.0.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
73
+ pywebexec-2.1.0.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
74
+ pywebexec-2.1.0.dist-info/RECORD,,