pywebexec 0.0.3__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.
@@ -0,0 +1,39 @@
1
+ # This workflow will upload a Python Package using Twine when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ # This workflow uses actions that are not certified by GitHub.
5
+ # They are provided by a third-party and are governed by
6
+ # separate terms of service, privacy policy, and support
7
+ # documentation.
8
+
9
+ name: Upload Python Package
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ deploy:
20
+
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v3
27
+ with:
28
+ python-version: '3.x'
29
+ - name: Install dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install build
33
+ - name: Build package
34
+ run: python -m build
35
+ - name: Publish package
36
+ uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37
+ with:
38
+ user: __token__
39
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,172 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # PyPI configuration file
171
+ .pypirc
172
+ pywebexec/version.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 joknarf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.1
2
+ Name: pywebexec
3
+ Version: 0.0.3
4
+ Summary: Simple Python HTTP Exec Server
5
+ Home-page: https://github.com/joknarf/pywebexec
6
+ Author: Franck Jouvanceau
7
+ Maintainer: Franck Jouvanceau
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 joknarf
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ Project-URL: Homepage, https://github.com/joknarf/pywebexec
31
+ Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
32
+ Project-URL: Repository, https://github.com/joknarf/pywebexec.git
33
+ Keywords: http,fileserver,browser,explorer
34
+ Classifier: Development Status :: 5 - Production/Stable
35
+ Classifier: Intended Audience :: System Administrators
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: POSIX
38
+ Classifier: Operating System :: Unix
39
+ Classifier: Operating System :: Microsoft :: Windows
40
+ Classifier: Operating System :: MacOS
41
+ Classifier: Programming Language :: Python
42
+ Classifier: Programming Language :: Python :: 3
43
+ Classifier: Programming Language :: Python :: 3.6
44
+ Classifier: Programming Language :: Python :: 3.7
45
+ Classifier: Programming Language :: Python :: 3.8
46
+ Classifier: Programming Language :: Python :: 3.9
47
+ Classifier: Programming Language :: Python :: 3.10
48
+ Classifier: Programming Language :: Python :: 3.11
49
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
50
+ Classifier: Topic :: System :: Clustering
51
+ Classifier: Topic :: System :: Networking
52
+ Classifier: Topic :: System :: Systems Administration
53
+ Requires-Python: >=3.6
54
+ Description-Content-Type: text/markdown
55
+ License-File: LICENSE
56
+ Requires-Dist: cryptography>=40.0.2
57
+ Requires-Dist: Flask>=3.0.3
58
+ Requires-Dist: Flask-HTTPAuth>=4.8.0
59
+
60
+ [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
61
+ ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
62
+ [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
63
+ [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
64
+ [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
65
+
66
+ # pywebexec
67
+ Simple Python HTTP(S) API/Web Command Launcher
68
+
69
+ ## Install
70
+ ```
71
+ $ pip install pywebexec
72
+ ```
73
+
74
+ ## Quick start
75
+
76
+ * start http server serving current directory executables listening on 0.0.0.0 port 8080
77
+ ```
78
+ $ pywebexec
79
+ ```
80
+
81
+ * Launch commands with params/view live output/Status using browser `http://<yourserver>:8080`
82
+
83
+ ## features
84
+
85
+ * Serve executables in current directory
86
+ * Launch commands with params from web browser
87
+ * Follow live output
88
+ * Stop command
89
+ * Relaunch command
90
+ * HTTPS support
91
+ * HTTPS self-signed certificate generator
92
+ * Can be started as a daemon (POSIX)
93
+ * uses gunicorn to serve http/https
94
+
95
+ ## Customize server
96
+ ```
97
+ $ pywebexec --listen 0.0.0.0 --port 8080
98
+ $ pywebexec -l 0.0.0.0 -p 8080
99
+ ```
100
+
101
+ ## Basic auth user/password
102
+ ```
103
+ $ pywebexec --user myuser [--password mypass]
104
+ $ pywebfs -u myuser [-P mypass]
105
+ ```
106
+ Generated password is given if no `--pasword` option
107
+
108
+ ## HTTPS server
109
+
110
+ * Generate auto-signed certificate and start https server
111
+ ```
112
+ $ pywebfs --gencert
113
+ $ pywebfs --g
114
+ ```
115
+
116
+ * Start https server using existing certificate
117
+ ```
118
+ $ pywebfs --cert /pathto/host.cert --key /pathto/host.key
119
+ $ pywebfs -c /pathto/host.cert -k /pathto/host.key
120
+ ```
121
+
122
+ ## Launch server as a daemon (Linux)
123
+
124
+ ```
125
+ $ pywebexec start
126
+ $ pywebexec status
127
+ $ pywebexec stop
128
+ ```
129
+ * log of server are stored in current directory `.web_status/pwexec_<listen>:<port>.log`
130
+
@@ -0,0 +1,71 @@
1
+ [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
2
+ ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
3
+ [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
4
+ [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
5
+ [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
6
+
7
+ # pywebexec
8
+ Simple Python HTTP(S) API/Web Command Launcher
9
+
10
+ ## Install
11
+ ```
12
+ $ pip install pywebexec
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ * start http server serving current directory executables listening on 0.0.0.0 port 8080
18
+ ```
19
+ $ pywebexec
20
+ ```
21
+
22
+ * Launch commands with params/view live output/Status using browser `http://<yourserver>:8080`
23
+
24
+ ## features
25
+
26
+ * Serve executables in current directory
27
+ * Launch commands with params from web browser
28
+ * Follow live output
29
+ * Stop command
30
+ * Relaunch command
31
+ * HTTPS support
32
+ * HTTPS self-signed certificate generator
33
+ * Can be started as a daemon (POSIX)
34
+ * uses gunicorn to serve http/https
35
+
36
+ ## Customize server
37
+ ```
38
+ $ pywebexec --listen 0.0.0.0 --port 8080
39
+ $ pywebexec -l 0.0.0.0 -p 8080
40
+ ```
41
+
42
+ ## Basic auth user/password
43
+ ```
44
+ $ pywebexec --user myuser [--password mypass]
45
+ $ pywebfs -u myuser [-P mypass]
46
+ ```
47
+ Generated password is given if no `--pasword` option
48
+
49
+ ## HTTPS server
50
+
51
+ * Generate auto-signed certificate and start https server
52
+ ```
53
+ $ pywebfs --gencert
54
+ $ pywebfs --g
55
+ ```
56
+
57
+ * Start https server using existing certificate
58
+ ```
59
+ $ pywebfs --cert /pathto/host.cert --key /pathto/host.key
60
+ $ pywebfs -c /pathto/host.cert -k /pathto/host.key
61
+ ```
62
+
63
+ ## Launch server as a daemon (Linux)
64
+
65
+ ```
66
+ $ pywebexec start
67
+ $ pywebexec status
68
+ $ pywebexec stop
69
+ ```
70
+ * log of server are stored in current directory `.web_status/pwexec_<listen>:<port>.log`
71
+
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "pywebexec"
3
+ authors = [
4
+ { name = "Franck Jouvanceau" },
5
+ ]
6
+ maintainers = [
7
+ { name = "Franck Jouvanceau" },
8
+ ]
9
+
10
+ description = "Simple Python HTTP Exec Server"
11
+ dependencies = [
12
+ "cryptography>=40.0.2",
13
+ "Flask>=3.0.3",
14
+ "Flask-HTTPAuth>=4.8.0",
15
+ ]
16
+ dynamic=["version"]
17
+ readme = "README.md"
18
+ license = {file = "LICENSE"}
19
+ requires-python = ">= 3.6"
20
+ keywords = ["http", "fileserver", "browser", "explorer"]
21
+ classifiers = [
22
+ "Development Status :: 5 - Production/Stable",
23
+ "Intended Audience :: System Administrators",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Operating System :: POSIX",
26
+ "Operating System :: Unix",
27
+ "Operating System :: Microsoft :: Windows ",
28
+ "Operating System :: MacOS",
29
+ "Programming Language :: Python",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.6",
32
+ "Programming Language :: Python :: 3.7",
33
+ "Programming Language :: Python :: 3.8",
34
+ "Programming Language :: Python :: 3.9",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ "Topic :: System :: Clustering",
39
+ "Topic :: System :: Networking",
40
+ "Topic :: System :: Systems Administration",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/joknarf/pywebexec"
45
+ Documentation = "https://github.com/joknarf/pywebexec/blob/main/README.md"
46
+ Repository = "https://github.com/joknarf/pywebexec.git"
47
+
48
+ [build-system]
49
+ requires = ["setuptools >= 61.0", "setuptools_scm[toml]>=6.2"]
50
+ build-backend = "setuptools.build_meta"
51
+
52
+ [tool.setuptools_scm]
53
+ version_file = "pywebexec/version.py"
54
+
55
+ [project.scripts]
56
+ pywebexec = "pywebexec.pywebexec:start_gunicorn"
@@ -0,0 +1,5 @@
1
+ """ package pywebexec """
2
+
3
+ __author__ = "Franck Jouvanceau"
4
+
5
+ from .pywebexec import start_gunicorn
@@ -0,0 +1,284 @@
1
+ from flask import Flask, request, jsonify, render_template
2
+ from flask_httpauth import HTTPBasicAuth
3
+ import subprocess
4
+ import threading
5
+ import os
6
+ import json
7
+ import uuid
8
+ import argparse
9
+ import random
10
+ import string
11
+ from datetime import datetime
12
+ import shlex
13
+ from gunicorn.app.base import BaseApplication
14
+
15
+ app = Flask(__name__)
16
+ auth = HTTPBasicAuth()
17
+
18
+ # Directory to store the script status and output
19
+ SCRIPT_STATUS_DIR = 'script_status'
20
+
21
+ if not os.path.exists(SCRIPT_STATUS_DIR):
22
+ os.makedirs(SCRIPT_STATUS_DIR)
23
+
24
+ def generate_random_password(length=12):
25
+ characters = string.ascii_letters + string.digits + string.punctuation
26
+ return ''.join(random.choice(characters) for i in range(length))
27
+
28
+ class StandaloneApplication(BaseApplication):
29
+
30
+ def __init__(self, app, options=None):
31
+ self.options = options or {}
32
+ self.application = app
33
+ super().__init__()
34
+
35
+ def load_config(self):
36
+ config = {
37
+ key: value for key, value in self.options.items()
38
+ if key in self.cfg.settings and value is not None
39
+ }
40
+ for key, value in config.items():
41
+ self.cfg.set(key.lower(), value)
42
+
43
+ def load(self):
44
+ return self.application
45
+
46
+
47
+ def start_gunicorn():
48
+ options = {
49
+ 'bind': '%s:%s' % (args.listen, args.port),
50
+ 'workers': 4,
51
+ 'certfile': args.cert,
52
+ 'keyfile': args.key,
53
+ }
54
+ StandaloneApplication(app, options=options).run()
55
+
56
+ def parseargs():
57
+ global app, args
58
+ parser = argparse.ArgumentParser(description='Run the script execution server.')
59
+ parser.add_argument('--user', help='Username for basic auth')
60
+ parser.add_argument('--password', help='Password for basic auth')
61
+ parser.add_argument(
62
+ "-l", "--listen", type=str, default="0.0.0.0", help="HTTP server listen address"
63
+ )
64
+ parser.add_argument(
65
+ "-p", "--port", type=int, default=8080, help="HTTP server listen port"
66
+ )
67
+ parser.add_argument(
68
+ "-d", "--dir", type=str, default=os.getcwd(), help="Serve target directory"
69
+ )
70
+ parser.add_argument(
71
+ "-t",
72
+ "--title",
73
+ type=str,
74
+ default="FileBrowser",
75
+ help="Web html title",
76
+ )
77
+ parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
78
+ parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
79
+
80
+ args = parser.parse_args()
81
+
82
+ if args.user:
83
+ app.config['USER'] = args.user
84
+ if args.password:
85
+ app.config['PASSWORD'] = args.password
86
+ else:
87
+ app.config['PASSWORD'] = generate_random_password()
88
+ print(f'Generated password for user {args.user}: {app.config["PASSWORD"]}')
89
+ else:
90
+ app.config['USER'] = None
91
+ app.config['PASSWORD'] = None
92
+ return args
93
+
94
+ parseargs()
95
+
96
+ def get_status_file_path(script_id):
97
+ return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}.json')
98
+
99
+ def get_output_file_path(script_id):
100
+ return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}_output.txt')
101
+
102
+ def update_script_status(script_id, status, script_name=None, params=None, start_time=None, end_time=None, exit_code=None):
103
+ status_file_path = get_status_file_path(script_id)
104
+ status_data = read_script_status(script_id) or {}
105
+ status_data['status'] = status
106
+ if script_name is not None:
107
+ status_data['script_name'] = script_name
108
+ if params is not None:
109
+ status_data['params'] = params
110
+ if start_time is not None:
111
+ status_data['start_time'] = start_time
112
+ if end_time is not None:
113
+ status_data['end_time'] = end_time
114
+ if exit_code is not None:
115
+ status_data['exit_code'] = exit_code
116
+ with open(status_file_path, 'w') as f:
117
+ json.dump(status_data, f)
118
+
119
+ def read_script_status(script_id):
120
+ status_file_path = get_status_file_path(script_id)
121
+ if not os.path.exists(status_file_path):
122
+ return None
123
+ with open(status_file_path, 'r') as f:
124
+ return json.load(f)
125
+
126
+ # Dictionary to store the process objects
127
+ processes = {}
128
+
129
+ def run_script(script_name, params, script_id):
130
+ start_time = datetime.now().isoformat()
131
+ update_script_status(script_id, 'running', script_name=script_name, params=params, start_time=start_time)
132
+ try:
133
+ output_file_path = get_output_file_path(script_id)
134
+ with open(output_file_path, 'w') as output_file:
135
+ # Run the script with parameters and redirect stdout and stderr to the file
136
+ process = subprocess.Popen([script_name] + params, stdout=output_file, stderr=output_file, text=True)
137
+ processes[script_id] = process
138
+ process.wait()
139
+ processes.pop(script_id, None)
140
+
141
+ end_time = datetime.now().isoformat()
142
+ # Update the status based on the result
143
+ if process.returncode == 0:
144
+ update_script_status(script_id, 'success', end_time=end_time, exit_code=process.returncode)
145
+ elif process.returncode == -15:
146
+ update_script_status(script_id, 'aborted', end_time=end_time, exit_code=process.returncode)
147
+ else:
148
+ update_script_status(script_id, 'failed', end_time=end_time, exit_code=process.returncode)
149
+ except Exception as e:
150
+ end_time = datetime.now().isoformat()
151
+ update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
152
+ with open(get_output_file_path(script_id), 'a') as output_file:
153
+ output_file.write(str(e))
154
+
155
+ def auth_required(f):
156
+ if app.config.get('USER'):
157
+ return auth.login_required(f)
158
+ return f
159
+
160
+ @app.route('/run_script', methods=['POST'])
161
+ @auth_required
162
+ def run_script_endpoint():
163
+ data = request.json
164
+ script_name = data.get('script_name')
165
+ params = data.get('params', [])
166
+
167
+ if not script_name:
168
+ return jsonify({'error': 'script_name is required'}), 400
169
+
170
+ # Ensure the script is an executable in the current directory
171
+ script_path = os.path.join(".", os.path.basename(script_name))
172
+ if not os.path.isfile(script_path) or not os.access(script_path, os.X_OK):
173
+ return jsonify({'error': 'script_name must be an executable in the current directory'}), 400
174
+
175
+ # Split params using shell-like syntax
176
+ try:
177
+ params = shlex.split(' '.join(params))
178
+ except ValueError as e:
179
+ return jsonify({'error': str(e)}), 400
180
+
181
+ # Generate a unique script_id
182
+ script_id = str(uuid.uuid4())
183
+
184
+ # Set the initial status to running and save script details
185
+ update_script_status(script_id, 'running', script_name, params)
186
+
187
+ # Run the script in a separate thread
188
+ thread = threading.Thread(target=run_script, args=(script_path, params, script_id))
189
+ thread.start()
190
+
191
+ return jsonify({'message': 'Script is running', 'script_id': script_id})
192
+
193
+ @app.route('/stop_script/<script_id>', methods=['POST'])
194
+ @auth_required
195
+ def stop_script(script_id):
196
+ process = processes.get(script_id)
197
+ end_time = datetime.now().isoformat()
198
+ if process and process.poll() is None:
199
+ try:
200
+ process.terminate()
201
+ process.wait() # Ensure the process has terminated
202
+ return jsonify({'message': 'Script aborted'})
203
+ except Exception as e:
204
+ status_data = read_script_status(script_id) or {}
205
+ status_data['status'] = 'failed'
206
+ status_data['end_time'] = end_time
207
+ status_data['exit_code'] = 1
208
+ with open(get_status_file_path(script_id), 'w') as f:
209
+ json.dump(status_data, f)
210
+ with open(get_output_file_path(script_id), 'a') as output_file:
211
+ output_file.write(str(e))
212
+ return jsonify({'error': 'Failed to terminate script'}), 500
213
+ update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
214
+ return jsonify({'error': 'Invalid script_id or script not running'}), 400
215
+
216
+ @app.route('/script_status/<script_id>', methods=['GET'])
217
+ @auth_required
218
+ def get_script_status(script_id):
219
+ status = read_script_status(script_id)
220
+ if not status:
221
+ return jsonify({'error': 'Invalid script_id'}), 404
222
+
223
+ output_file_path = get_output_file_path(script_id)
224
+ if os.path.exists(output_file_path):
225
+ with open(output_file_path, 'r') as output_file:
226
+ output = output_file.read()
227
+ status['output'] = output
228
+
229
+ return jsonify(status)
230
+
231
+ @app.route('/')
232
+ @auth_required
233
+ def index():
234
+ return render_template('index.html')
235
+
236
+ @app.route('/scripts', methods=['GET'])
237
+ @auth_required
238
+ def list_scripts():
239
+ scripts = []
240
+ for filename in os.listdir(SCRIPT_STATUS_DIR):
241
+ if filename.endswith('.json'):
242
+ script_id = filename[:-5]
243
+ status = read_script_status(script_id)
244
+ if status:
245
+ command = status['script_name'] + ' ' + shlex.join(status['params'])
246
+ scripts.append({
247
+ 'script_id': script_id,
248
+ 'status': status['status'],
249
+ 'start_time': status.get('start_time', 'N/A'),
250
+ 'end_time': status.get('end_time', 'N/A'),
251
+ 'command': command,
252
+ 'exit_code': status.get('exit_code', 'N/A')
253
+ })
254
+ # Sort scripts by start_time in descending order
255
+ scripts.sort(key=lambda x: x['start_time'], reverse=True)
256
+ return jsonify(scripts)
257
+
258
+ @app.route('/script_output/<script_id>', methods=['GET'])
259
+ @auth_required
260
+ def get_script_output(script_id):
261
+ output_file_path = get_output_file_path(script_id)
262
+ if os.path.exists(output_file_path):
263
+ with open(output_file_path, 'r') as output_file:
264
+ output = output_file.read()
265
+ status_data = read_script_status(script_id) or {}
266
+ return jsonify({'output': output, 'status': status_data.get("status")})
267
+ return jsonify({'error': 'Invalid script_id'}), 404
268
+
269
+ @app.route('/executables', methods=['GET'])
270
+ @auth_required
271
+ def list_executables():
272
+ executables = [f for f in os.listdir('.') if os.path.isfile(f) and os.access(f, os.X_OK)]
273
+ return jsonify(executables)
274
+
275
+ @auth.verify_password
276
+ def verify_password(username, password):
277
+ return username == app.config['USER'] and password == app.config['PASSWORD']
278
+
279
+
280
+
281
+
282
+ if __name__ == '__main__':
283
+ start_gunicorn()
284
+ #app.run(host='0.0.0.0', port=5000)
@@ -0,0 +1 @@
1
+ <svg viewBox="-1 -1 13 13" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M6 12A6 6 0 106 0a6 6 0 000 12zM3 5a1 1 0 000 2h6a1 1 0 100-2H3z" fill="#ff641a"></path></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="25" height="16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="25" height="16" fill="currentColor" style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"><path fill="#118811" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="-2 -2 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><title>cross-circle</title><desc>Created with Sketch Beta.</desc><defs></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"><g id="Icon-Set-Filled" sketch:type="MSLayerGroup" transform="translate(-570.000000, -1089.000000)" fill="#ca0000"><path d="M591.657,1109.24 C592.048,1109.63 592.048,1110.27 591.657,1110.66 C591.267,1111.05 590.633,1111.05 590.242,1110.66 L586.006,1106.42 L581.74,1110.69 C581.346,1111.08 580.708,1111.08 580.314,1110.69 C579.921,1110.29 579.921,1109.65 580.314,1109.26 L584.58,1104.99 L580.344,1100.76 C579.953,1100.37 579.953,1099.73 580.344,1099.34 C580.733,1098.95 581.367,1098.95 581.758,1099.34 L585.994,1103.58 L590.292,1099.28 C590.686,1098.89 591.323,1098.89 591.717,1099.28 C592.11,1099.68 592.11,1100.31 591.717,1100.71 L587.42,1105.01 L591.657,1109.24 L591.657,1109.24 Z M586,1089 C577.163,1089 570,1096.16 570,1105 C570,1113.84 577.163,1121 586,1121 C594.837,1121 602,1113.84 602,1105 C602,1096.16 594.837,1089 586,1089 L586,1089 Z" id="cross-circle" sketch:type="MSShapeGroup"></path></g></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M661.333333 170.666667l253.866667 34.133333-209.066667 209.066667zM362.666667 853.333333L108.8 819.2l209.066667-209.066667zM170.666667 362.666667L204.8 108.8l209.066667 209.066667z" fill="#4949d1"></path><path d="M198.4 452.266667l-89.6 17.066666c-2.133333 14.933333-2.133333 27.733333-2.133333 42.666667 0 98.133333 34.133333 192 98.133333 264.533333l64-55.466666C219.733333 663.466667 192 588.8 192 512c0-19.2 2.133333-40.533333 6.4-59.733333zM512 106.666667c-115.2 0-217.6 49.066667-292.266667 125.866666l59.733334 59.733334C339.2 230.4 420.266667 192 512 192c19.2 0 40.533333 2.133333 59.733333 6.4l14.933334-83.2C563.2 108.8 537.6 106.666667 512 106.666667zM825.6 571.733333l89.6-17.066666c2.133333-14.933333 2.133333-27.733333 2.133333-42.666667 0-93.866667-32-185.6-91.733333-258.133333l-66.133333 53.333333c46.933333 57.6 72.533333 130.133333 72.533333 202.666667 0 21.333333-2.133333 42.666667-6.4 61.866666zM744.533333 731.733333C684.8 793.6 603.733333 832 512 832c-19.2 0-40.533333-2.133333-59.733333-6.4l-14.933334 83.2c25.6 4.266667 51.2 6.4 74.666667 6.4 115.2 0 217.6-49.066667 292.266667-125.866667l-59.733334-57.6z" fill="#4949d1"></path><path d="M853.333333 661.333333l-34.133333 253.866667-209.066667-209.066667z" fill="#4949d1"></path></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 75 949 949" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill="#00a600" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336L456.192 600.384z"></path></g></svg>
File without changes
@@ -0,0 +1,265 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>pywebexec</title>
6
+ <style>
7
+ body { font-family: Arial, sans-serif; }
8
+ .table-container { max-height: 385px; overflow-y: auto; }
9
+ table { width: 100%; border-collapse: collapse; }
10
+ th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
11
+ th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
12
+ .output {
13
+ white-space: pre-wrap;
14
+ background: #f0f0f0;
15
+ padding: 10px;
16
+ border: 1px solid #ccc;
17
+ font-family: monospace;
18
+ border-radius: 15px;
19
+ }
20
+ .copy-icon { cursor: pointer; }
21
+ .monospace { font-family: monospace; }
22
+ .copied { color: green; margin-left: 5px; }
23
+ button {
24
+ -webkit-appearance: none;
25
+ -webkit-border-radius: none;
26
+ appearance: none;
27
+ border-radius: 15px;
28
+ padding: 3px;
29
+ padding-right: 13px;
30
+ border: 1px #555 solid;
31
+ height: 22px;
32
+ font-size: 13px;
33
+ outline: none;
34
+ text-indent: 10px;
35
+ background-color: #eee;
36
+ display: inline-block;
37
+ vertical-align: middle;
38
+ }
39
+ form {
40
+ padding-bottom: 15px;
41
+ }
42
+ .status-icon {
43
+ display: inline-block;
44
+ width: 16px;
45
+ height: 16px;
46
+ margin-right: 5px;
47
+ background-size: contain;
48
+ background-repeat: no-repeat;
49
+ vertical-align: middle;
50
+ }
51
+ .status-running {
52
+ background-image: url("/static/images/running.svg")
53
+ }
54
+ .status-success {
55
+ background-image: url("/static/images/success.svg")
56
+ }
57
+ .status-failed {
58
+ background-image: url("/static/images/failed.svg")
59
+ }
60
+ .status-aborted {
61
+ background-image: url("/static/images/aborted.svg")
62
+ }
63
+ .copy_clip {
64
+ padding-right: 25px;
65
+ background-repeat: no-repeat;
66
+ background-position: right top;
67
+ background-size: 25px 16px;
68
+ white-space: nowrap;
69
+ }
70
+ .copy_clip_left {
71
+ padding-left: 25px;
72
+ padding-right: 0px;
73
+ background-position: left top;
74
+ }
75
+ .copy_clip:hover {
76
+ cursor: pointer;
77
+ background-image: url("/static/images/copy.svg");
78
+ }
79
+ .copy_clip_ok, .copy_clip_ok:hover {
80
+ background-image: url("/static/images/copy_ok.svg");
81
+ }
82
+
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <h1>pywebexec</h1>
87
+ <form id="launchForm">
88
+ <label for="scriptName">Command:</label>
89
+ <select id="scriptName" name="scriptName"></select>
90
+ <label for="params">Params:</label>
91
+ <input type="text" id="params" name="params">
92
+ <button type="submit">Launch</button>
93
+ </form>
94
+ <div class="table-container">
95
+ <table>
96
+ <thead>
97
+ <tr>
98
+ <th>Script ID</th>
99
+ <th>Status</th>
100
+ <th>Start Time</th>
101
+ <th>Duration</th>
102
+ <th>Exit</th>
103
+ <th>Command</th>
104
+ <th>Actions</th>
105
+ </tr>
106
+ </thead>
107
+ <tbody id="scripts"></tbody>
108
+ </table>
109
+ </div>
110
+ <div id="output" class="output"></div>
111
+
112
+ <script>
113
+ let currentScriptId = null;
114
+ let outputInterval = null;
115
+
116
+ document.getElementById('launchForm').addEventListener('submit', async (event) => {
117
+ event.preventDefault();
118
+ const scriptName = document.getElementById('scriptName').value;
119
+ const params = document.getElementById('params').value.split(' ');
120
+ const response = await fetch('/run_script', {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json'
124
+ },
125
+ body: JSON.stringify({ script_name: scriptName, params: params })
126
+ });
127
+ const data = await response.json();
128
+ fetchScripts();
129
+ });
130
+
131
+ async function fetchScripts() {
132
+ const response = await fetch('/scripts');
133
+ const scripts = await response.json();
134
+ scripts.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
135
+ const scriptsTbody = document.getElementById('scripts');
136
+ scriptsTbody.innerHTML = '';
137
+ scripts.forEach(script => {
138
+ const scriptRow = document.createElement('tr');
139
+ scriptRow.innerHTML = `
140
+ <td class="monospace">
141
+ <span class="copy_clip" onclick="copyToClipboard('${script.script_id}', this)">${script.script_id.slice(0, 8)}</span>
142
+ </td>
143
+ <td><span class="status-icon status-${script.status}"></span>${script.status}</td>
144
+ <td>${formatTime(script.start_time)}</td>
145
+ <td>${script.status === 'running' ? formatDuration(script.start_time, new Date().toISOString()) : formatDuration(script.start_time, script.end_time)}</td>
146
+ <td>${script.exit_code}</td>
147
+ <td>${script.command.replace(/^\.\//, '')}</td>
148
+ <td>
149
+ <button onclick="viewOutput('${script.script_id}')">Log</button>
150
+ <button onclick="relaunchScript('${script.script_id}')">Relaunch</button>
151
+ ${script.status === 'running' ? `<button onclick="stopScript('${script.script_id}')">Stop</button>` : ''}
152
+ </td>
153
+ `;
154
+ scriptsTbody.appendChild(scriptRow);
155
+ });
156
+ }
157
+
158
+ async function fetchExecutables() {
159
+ const response = await fetch('/executables');
160
+ const executables = await response.json();
161
+ const scriptNameSelect = document.getElementById('scriptName');
162
+ scriptNameSelect.innerHTML = '';
163
+ executables.forEach(executable => {
164
+ const option = document.createElement('option');
165
+ option.value = executable;
166
+ option.textContent = executable;
167
+ scriptNameSelect.appendChild(option);
168
+ });
169
+ }
170
+
171
+ async function fetchOutput(script_id) {
172
+ const outputDiv = document.getElementById('output');
173
+ const response = await fetch(`/script_output/${script_id}`);
174
+ const data = await response.json();
175
+ if (data.error) {
176
+ outputDiv.innerHTML = data.error;
177
+ clearInterval(outputInterval);
178
+ } else {
179
+ outputDiv.innerHTML = data.output;
180
+ if (data.status != 'running') {
181
+ clearInterval(outputInterval)
182
+ }
183
+ }
184
+ }
185
+
186
+ async function viewOutput(script_id) {
187
+ currentScriptId = script_id;
188
+ clearInterval(outputInterval);
189
+ const response = await fetch(`/script_status/${script_id}`);
190
+ const data = await response.json();
191
+ if (data.status === 'running') {
192
+ fetchOutput(script_id);
193
+ outputInterval = setInterval(() => fetchOutput(script_id), 1000);
194
+ } else {
195
+ fetchOutput(script_id);
196
+ }
197
+ }
198
+
199
+ async function relaunchScript(script_id) {
200
+ const response = await fetch(`/script_status/${script_id}`);
201
+ const data = await response.json();
202
+ if (data.error) {
203
+ alert(data.error);
204
+ return;
205
+ }
206
+ const relaunchResponse = await fetch('/run_script', {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json'
210
+ },
211
+ body: JSON.stringify({
212
+ script_name: data.script_name,
213
+ params: data.params
214
+ })
215
+ });
216
+ const relaunchData = await relaunchResponse.json();
217
+ alert(relaunchData.message);
218
+ fetchScripts();
219
+ }
220
+
221
+ async function stopScript(script_id) {
222
+ const response = await fetch(`/stop_script/${script_id}`, {
223
+ method: 'POST'
224
+ });
225
+ const data = await response.json();
226
+ if (data.error) {
227
+ alert(data.error);
228
+ } else {
229
+ alert(data.message);
230
+ fetchScripts();
231
+ }
232
+ }
233
+
234
+ function formatTime(time) {
235
+ if (!time || time === 'N/A') return 'N/A';
236
+ const date = new Date(time);
237
+ return date.toISOString().slice(0, 16).replace('T', ' ');
238
+ }
239
+
240
+ function formatDuration(startTime, endTime) {
241
+ if (!startTime || !endTime) return 'N/A';
242
+ const start = new Date(startTime);
243
+ const end = new Date(endTime);
244
+ const duration = (end - start) / 1000;
245
+ const hours = Math.floor(duration / 3600);
246
+ const minutes = Math.floor((duration % 3600) / 60);
247
+ const seconds = Math.floor(duration % 60);
248
+ return `${hours}h ${minutes}m ${seconds}s`;
249
+ }
250
+
251
+ function copyToClipboard(text, element) {
252
+ navigator.clipboard.writeText(text).then(() => {
253
+ element.classList.add('copy_clip_ok')
254
+ setTimeout(() => {
255
+ element.classList.remove('copy_clip_ok');
256
+ }, 2000);
257
+ });
258
+ }
259
+
260
+ fetchScripts();
261
+ fetchExecutables();
262
+ setInterval(fetchScripts, 5000);
263
+ </script>
264
+ </body>
265
+ </html>
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.0.3'
16
+ __version_tuple__ = version_tuple = (0, 0, 3)
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.1
2
+ Name: pywebexec
3
+ Version: 0.0.3
4
+ Summary: Simple Python HTTP Exec Server
5
+ Home-page: https://github.com/joknarf/pywebexec
6
+ Author: Franck Jouvanceau
7
+ Maintainer: Franck Jouvanceau
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 joknarf
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ Project-URL: Homepage, https://github.com/joknarf/pywebexec
31
+ Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
32
+ Project-URL: Repository, https://github.com/joknarf/pywebexec.git
33
+ Keywords: http,fileserver,browser,explorer
34
+ Classifier: Development Status :: 5 - Production/Stable
35
+ Classifier: Intended Audience :: System Administrators
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: POSIX
38
+ Classifier: Operating System :: Unix
39
+ Classifier: Operating System :: Microsoft :: Windows
40
+ Classifier: Operating System :: MacOS
41
+ Classifier: Programming Language :: Python
42
+ Classifier: Programming Language :: Python :: 3
43
+ Classifier: Programming Language :: Python :: 3.6
44
+ Classifier: Programming Language :: Python :: 3.7
45
+ Classifier: Programming Language :: Python :: 3.8
46
+ Classifier: Programming Language :: Python :: 3.9
47
+ Classifier: Programming Language :: Python :: 3.10
48
+ Classifier: Programming Language :: Python :: 3.11
49
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
50
+ Classifier: Topic :: System :: Clustering
51
+ Classifier: Topic :: System :: Networking
52
+ Classifier: Topic :: System :: Systems Administration
53
+ Requires-Python: >=3.6
54
+ Description-Content-Type: text/markdown
55
+ License-File: LICENSE
56
+ Requires-Dist: cryptography>=40.0.2
57
+ Requires-Dist: Flask>=3.0.3
58
+ Requires-Dist: Flask-HTTPAuth>=4.8.0
59
+
60
+ [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
61
+ ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
62
+ [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
63
+ [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
64
+ [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
65
+
66
+ # pywebexec
67
+ Simple Python HTTP(S) API/Web Command Launcher
68
+
69
+ ## Install
70
+ ```
71
+ $ pip install pywebexec
72
+ ```
73
+
74
+ ## Quick start
75
+
76
+ * start http server serving current directory executables listening on 0.0.0.0 port 8080
77
+ ```
78
+ $ pywebexec
79
+ ```
80
+
81
+ * Launch commands with params/view live output/Status using browser `http://<yourserver>:8080`
82
+
83
+ ## features
84
+
85
+ * Serve executables in current directory
86
+ * Launch commands with params from web browser
87
+ * Follow live output
88
+ * Stop command
89
+ * Relaunch command
90
+ * HTTPS support
91
+ * HTTPS self-signed certificate generator
92
+ * Can be started as a daemon (POSIX)
93
+ * uses gunicorn to serve http/https
94
+
95
+ ## Customize server
96
+ ```
97
+ $ pywebexec --listen 0.0.0.0 --port 8080
98
+ $ pywebexec -l 0.0.0.0 -p 8080
99
+ ```
100
+
101
+ ## Basic auth user/password
102
+ ```
103
+ $ pywebexec --user myuser [--password mypass]
104
+ $ pywebfs -u myuser [-P mypass]
105
+ ```
106
+ Generated password is given if no `--pasword` option
107
+
108
+ ## HTTPS server
109
+
110
+ * Generate auto-signed certificate and start https server
111
+ ```
112
+ $ pywebfs --gencert
113
+ $ pywebfs --g
114
+ ```
115
+
116
+ * Start https server using existing certificate
117
+ ```
118
+ $ pywebfs --cert /pathto/host.cert --key /pathto/host.key
119
+ $ pywebfs -c /pathto/host.cert -k /pathto/host.key
120
+ ```
121
+
122
+ ## Launch server as a daemon (Linux)
123
+
124
+ ```
125
+ $ pywebexec start
126
+ $ pywebexec status
127
+ $ pywebexec stop
128
+ ```
129
+ * log of server are stored in current directory `.web_status/pwexec_<listen>:<port>.log`
130
+
@@ -0,0 +1,23 @@
1
+ .gitignore
2
+ LICENSE
3
+ README.md
4
+ pyproject.toml
5
+ setup.cfg
6
+ .github/workflows/python-publish.yml
7
+ pywebexec/__init__.py
8
+ pywebexec/pywebexec.py
9
+ pywebexec/version.py
10
+ pywebexec.egg-info/PKG-INFO
11
+ pywebexec.egg-info/SOURCES.txt
12
+ pywebexec.egg-info/dependency_links.txt
13
+ pywebexec.egg-info/entry_points.txt
14
+ pywebexec.egg-info/requires.txt
15
+ pywebexec.egg-info/top_level.txt
16
+ pywebexec/static/images/aborted.svg
17
+ pywebexec/static/images/copy.svg
18
+ pywebexec/static/images/copy_ok.svg
19
+ pywebexec/static/images/failed.svg
20
+ pywebexec/static/images/running.svg
21
+ pywebexec/static/images/success.svg
22
+ pywebexec/templates/__init__.py
23
+ pywebexec/templates/index.html
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pywebexec = pywebexec.pywebexec:start_gunicorn
@@ -0,0 +1,3 @@
1
+ cryptography>=40.0.2
2
+ Flask>=3.0.3
3
+ Flask-HTTPAuth>=4.8.0
@@ -0,0 +1 @@
1
+ pywebexec
@@ -0,0 +1,7 @@
1
+ [metadata]
2
+ home_page = https://github.com/joknarf/pywebexec
3
+
4
+ [egg_info]
5
+ tag_build =
6
+ tag_date = 0
7
+