├── .gitignore ├── LICENSE ├── README-V0.md ├── README.md ├── setup.py ├── update-static.sh └── vuejspython ├── __init__.py ├── builtin ├── create-demo-files.vue ├── diff.min.js ├── edit-file.vue ├── rotate-files.vue ├── serve-videos.vue ├── toggle-exam.vue └── view-file.vue ├── fsapi.js ├── index.html └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | unzip-*/ 2 | epoch*/ 3 | build/ 4 | dist/ 5 | __pycache__/ 6 | *.egg-info/ 7 | demo/ 8 | .params.json 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rémi EMONET 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. 22 | -------------------------------------------------------------------------------- /README-V0.md: -------------------------------------------------------------------------------- 1 | 2 | Vuejs-python brings the concepts of vuejs to Python. 3 | You can write your model/data as a Python object and your HTML UI/view with the very convenient vuejs syntax. 4 | As soon as part of your model changes, all dependent variables are updated and the HTML UI is automatically refreshed. 5 | 6 | The goal is to use not only Python the language but Python the platform (with numpy, system APIs, and other "native" things). 7 | 8 | ## Installation 9 | 10 | ~~~ 11 | pip install vuejspython 12 | ~~~ 13 | 14 | ## Tiny Example 15 | 16 | You need to create two files: one Python model and an HTML UI. 17 | A good convention (to help tools) is to use the same name, with the `.py` and `.html` extensions, respectively. 18 | 19 |
20 |
21 | 22 | `# example.py` 23 | 24 | ```python 25 | ... 26 | @model 27 | class App: 28 | radius = 5 29 | def computed_area(self): 30 | return pi * self.radius ** 2 31 | 32 | vuejspython.start(App()) 33 | ``` 34 | 35 |
36 |
37 | 38 | `# example.html` 39 | 40 | ```html 41 |
42 | Fill the radius in the text field: . 43 | (or with
44 | A disk with radius {{ radius }} has an area of {{ area }}. 45 |
46 |   47 | 48 | 49 | ``` 50 | 51 |
52 |
53 | 54 | ## Running, option 1: only with Python 55 | 56 | ~~~bash 57 | python3 example.py 58 | 59 | # or a tiny shell function helper 60 | pvue() { (sleep .5;firefox ${1%.*}.html)& python3 ${1%.*}.py;} 61 | pvue example.py 62 | ~~~ 63 | 64 | This will give you an address to which you should append your HTML file name, here `example.html`. 65 | In this example, you will visit 66 | (or visit the given address `http://localhost:4260` and click your file). 67 | 68 | NB: you need to stop the command with `Ctrl+C` if you want to run another example. 69 | 70 | 71 | ## Running, option 2: with hot reload on file change 72 | 73 | Here we will start two processes, one for the HTML part (with live reload, and another only for the Python). 74 | 75 | Terminal 1, hosting the HTML files with hot reload: 76 | 77 | ~~~bash 78 | # one-time install 79 | pip install watchdog 80 | npm install -g simple-hot-reload-server 81 | # in terminal 1 (hot html reload, for all files) 82 | hrs . 83 | ~~~ 84 | (this gives you the address to open, after appending your file name, e.g., ) 85 | 86 | Terminal 2, running the python server 87 | 88 | ~~~bash 89 | # in terminal 2 (start or restart python) 90 | NOSERVE=1 python3 example.py 91 | # OR, for live restart 92 | NOSERVE=1 watchmedo auto-restart --patterns="*.py" python3 example.py 93 | ~~~ 94 | NB: `NOSERVE=1` tells vuejspython to not serve the HTML files (it is handled by `hrs` above) 95 | 96 | NB: when changing the .py, a manual browser refresh is still needed, see below for a more complete solution 97 | 98 | ### Helper for complete live reload (live restart for python) 99 | 100 | ~~~bash 101 | pvue() { if test "$1" = open ; then shift ; (sleep 1 ; firefox "http://localhost:8082/${1%.*}.html") & fi; watchmedo auto-restart --patterns="*.py" --ignore-patterns="*/.#*.py" bash -- -c '(sleep .250 ; touch '"${1%.*}"'.html) & python3 '"${1%.*}"'.py' ; } 102 | # Then 103 | pvue example 104 | # OR to also open firefox 105 | pvue open example 106 | # OR some convenient variations 107 | NOSERVE=1 pvue open example 108 | pvue open example. 109 | pvue open example.py 110 | pvue open example.html 111 | # it always runs the file with the .py extension 112 | ~~~ 113 | 114 | 115 | ## Other, different projects 116 | 117 | If you're interested only in using Python the language with Vue.js, you can try [brython](http://brython.info/) and the [brython vue demo](http://brython.info/gallery/test_vue.html) 118 | 119 | There are projects that try to help integrating Vue.js with different Python web frameworks. The goal is different: Vuejs-python makes python and vue tightly integrated, in a common, reactive model. 120 | 121 | ---- 122 | 123 | 124 | ## Development (TO BE REVIEWED AND UPDATED) 125 | 126 | ### Requirements 127 | 128 | ~~~ bash 129 | pip install aiohttp 130 | pip install websockets 131 | ~~~ 132 | 133 | You also need to get a few libraries: 134 | 135 | ~~~ 136 | cd vuejspython 137 | ./update-static.sh # or manually download the files 138 | ~~~ 139 | 140 | ### Notes 141 | 142 | Currently, the project uses requires some "observable collections", with some modifications. 143 | They are included in the package, in `vuejspython/observablecollections`. 144 | They have been obtained with: 145 | 146 | ~~~ bash 147 | git clone https://github.com/fousteris-dim/Python-observable-collections.git observablecollections 148 | patch -d observablecollections/ < 0001-Local-imports.patch 149 | ~~~ 150 | 151 | 152 | 153 | ## Helpers 154 | 155 | ### Simply launch python and open a browser (firefox) at the right address. 156 | 157 | ~~~ bash 158 | # bash function 159 | pvue() { (sleep .5;firefox ${1%.*}.html)& python3 ${1%.*}.py;} 160 | 161 | # examples 162 | pvue example-1 163 | pvue example-1. 164 | pvue example-1.html 165 | pvue example-1.py 166 | ~~~ 167 | 168 | 169 | ### OR, to develop with auto-reload. 170 | 171 | ~~~ bash 172 | # one-time install 173 | pip install watchdog 174 | npm install -g simple-hot-reload-server 175 | 176 | 177 | # in terminal 1 (hot html reload, for all files) 178 | hrs . 179 | 180 | # in terminal 2 (start and restart python 181 | pvue() { if test "$1" = open ; then shift ; (sleep 1 ; firefox "http://localhost:8082/${1%.*}.html") & fi; watchmedo auto-restart --patterns="*.py" --ignore-patterns="*/.#*.py" bash -- -c '(sleep .250 ; touch '"${1%.*}"'.html) & python3 '"${1%.*}"'.py' ; } 182 | pvue open example-1 183 | pvue example-1 184 | # the first opens firefox initially 185 | ~~~ 186 | 187 | 188 | ## Pypi stuff 189 | 190 | ~~~ 191 | python3 -m pip install --upgrade setuptools wheel 192 | python3 -m pip install --upgrade twine 193 | 194 | # update the version number in setup.py and then 195 | rm -rf dist/ 196 | python3 setup.py sdist bdist_wheel 197 | python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 198 | # OR: python3 -m twine upload dist/* 199 | 200 | python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps vuejspython 201 | 202 | pip uninstall vuejspython 203 | 204 | python3 -m pip install .. ; ll $VENV/lib/python3.6/site-packages/vuejspython 205 | ~~~ 206 | 207 | 208 | ## TODO 209 | 210 | - see why example-8 is slow with the python-only server 211 | - test atomic in components 212 | - make it an all-components (no too-special ROOT', with also js that tells what class and package/file (import everything manually in a main.py if not easy) is the root -> this way we can run a single python and have multiple demos 213 | - have a clean solution for the observable collections (integrate and clean minimal code or find another lib) 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | If you're looking for "Vuejs-python that brings the concepts of vuejs to Python", see our V0 branch (and readme, and V0.* on pypi). 3 | 4 | The goal of vuejs-python (starting from V1) is to 5 | - easily run standalone Vue.js components / micro-apps, 6 | - give them access to the working directory, 7 | - allow (unsafe) extensions to be written in Python (including numpy, local resource access, etc) 8 | 9 | 10 | ## Installation 11 | 12 | ~~~ 13 | pip install vuejspython 14 | ~~~ 15 | 16 | ## Usage 17 | 18 | ~~~ 19 | vjspy [--trust-python] [--port=...] [--host=...] myfile.vue more arbitrary parameters 20 | ~~~ 21 | 22 | 23 | You can run the bundled tools using their `:` prefixed names instead of the vue file. 24 | These files are in https://github.com/twitwi/vuejs-python/tree/main/vuejspython/builtin 25 | 26 | Here are some example usage, that can be followed as a tutorial: 27 | 28 | ~~~ 29 | vjspy :create-demo-files demo 30 | vjspy :view-file demo/file1.txt 31 | vjspy :edit-file demo/file1.txt 32 | vjspy --trust-python :rotate-files demo/file*.txt 33 | vjspy :toggle-exam demo/exam/* 34 | 35 | # hosting the current folder with a video player 36 | vjspy :serve-videos 37 | ~~~ 38 | 39 | ## Snippets 40 | 41 | ### HTML head 42 | 43 | To add something in the HTML head (change title, icon, load library, etc): 44 | 45 | ~~~html 46 | 55 | ~~~ 56 | 57 | ### Accessing Vue 58 | 59 | To import elements from the bundled Vue (you can remove `lang="ts"` to use plain js, and setup to use a traditional component writing): 60 | 61 | ~~~html 62 | 66 | ~~~ 67 | 68 | ### Accessing files 69 | 70 | To use the custom filesystem API ([asynchronously](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Async_JS/Promises)): 71 | 72 | ~~~javascript 73 | 106 | 107 | 113 | ~~~ 114 | 115 | See also the [rotate-files example](https://github.com/twitwi/vuejs-python/tree/main/vuejspython/builtin/rotate-files.vue) that uses command line arguments (in python) too. 116 | 117 | 118 | ### Handling url hash 119 | 120 | ~~~javascript 121 | { 125 | // also triggers on first load (with empty hash or the hash from the initial URL) 126 | if (h !== '') { 127 | alert('you changed the address to '+h) 128 | } 129 | }) 130 | 131 | function showAbout() { 132 | ... 133 | setHash('about') 134 | } 135 | ~~~ 136 | 137 | 138 | 139 | ---- 140 | 141 | 142 | 143 | 144 | ## Pypi stuff 145 | 146 | (in a clean clone, to be sure that not too much thing are copied) 147 | 148 | ~~~ 149 | python3 -m pip install --upgrade setuptools wheel 150 | python3 -m pip install --upgrade twine 151 | 152 | 153 | # update the version number in setup.py and then 154 | ./update-static.sh 155 | rm -rf dist/ 156 | python3 setup.py sdist bdist_wheel 157 | python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 158 | # OR: python3 -m twine upload dist/* 159 | 160 | python3 -m pip install --index-url https://test.pypi.org/simple/ --upgrade --no-deps vuejspython 161 | # OR: python3 -m pip install --upgrade --no-deps vuejspython 162 | 163 | pip uninstall vuejspython 164 | 165 | python3 -m pip install .. ; ll $VENV/lib/python3.6/site-packages/vuejspython 166 | ~~~ 167 | 168 | 169 | 170 | ## TODO 171 | 172 | 173 | - [ ] maybe integrate zod? 174 | - [ ] doc that some thing are bunlded or with e.g. /.runner/builtin/diff.min.js ... consider e.g. #diff in addition for these... yes, like the #fsapi (which is built-in too) 175 | - [ ] example: exam, diff does not update/clear on save 176 | - [ ] Allow --online to get latest libs from cdn 177 | - [ ] Allow --open-browser to open the file 178 | - [ ] Allow opening an existing instance if the port is taken (already running) 179 | - [ ] Allow a random port 180 | - [ ] Warn if some python but no --trust-python (and neither --no-pyton) and show the updated command 181 | - [ ] Allow some config section, e.g. with prefered port etc 182 | - [ ] Update build system... python setup.py is deprecated 183 | - [ ] When trust-python, expose an api to run shell commands too (maybe proto in trailtools) 184 | - [ ] consider integration of .quit (from trailtools logdown / sport) 185 | - [ ] an eject command to get the source of a bultin 186 | - [ ] a @pkg/exc to allow for running a custom one, e.g. that have been vjspy install github:twitwi/blabla... or rather pip installed! (with maybe a default per package? so just @pkg is ok?) 187 | - [ ] video: allow for a logo 188 | - [ ] video: filter files (e.g. remove extensionless things, vtt too, etc, optionnal, still allow to show all with a checkbox) 189 | 190 | BETTER FS API 191 | 192 | - [ ] Allow --allow-all-files 193 | - [ ] Nicer not-all-recursive listing... for perfs 194 | - [ ] Also add watcher option (need websockets probably, or add a polling e.g. /.changed /.last-change) 195 | - [ ] consider use as vite-based projects too? and/or a version that hosts a build page but allows the fs api etc? and/or an fsapi that works similarly but in vite? see https://github.com/StarLederer/vite-plugin-fs (for inspiration or direct use) or 4y https://github.com/antfu/vite-fs 196 | 197 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read().split('----')[0] 5 | 6 | setuptools.setup( 7 | name="vuejspython", 8 | version="1.0.6", 9 | author="Rémi Emonet", 10 | author_email="remi+242-e2f8@heeere.com", 11 | description="Vuejs runner, with filesystem, and allowing python extension (e.g., to leverage numpy)", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/twitwi/vuejs-python/", 15 | install_requires=['fastapi', 'uvicorn'], 16 | packages=setuptools.find_packages(), 17 | package_data={ 18 | 'vuejspython': ['index.html', '*.js', 'builtin/*'], 19 | }, 20 | entry_points = { 21 | 'console_scripts': [ 22 | 'vjspy = vuejspython.run:cli', 23 | ], 24 | }, 25 | classifiers=[ 26 | "Programming Language :: Python :: 3", 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: OS Independent", 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /update-static.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat < /dev/null 4 | 113 | -------------------------------------------------------------------------------- /vuejspython/builtin/diff.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | diff v5.1.0 4 | 5 | Software License Agreement (BSD License) 6 | 7 | Copyright (c) 2009-2015, Kevin Decker 8 | 9 | All rights reserved. 10 | 11 | Redistribution and use of this software in source and binary forms, with or without modification, 12 | are permitted provided that the following conditions are met: 13 | 14 | * Redistributions of source code must retain the above 15 | copyright notice, this list of conditions and the 16 | following disclaimer. 17 | 18 | * Redistributions in binary form must reproduce the above 19 | copyright notice, this list of conditions and the 20 | following disclaimer in the documentation and/or other 21 | materials provided with the distribution. 22 | 23 | * Neither the name of Kevin Decker nor the names of its 24 | contributors may be used to endorse or promote products 25 | derived from this software without specific prior 26 | written permission. 27 | 28 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 29 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 30 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 31 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 34 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 35 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | @license 37 | */ 38 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e=e||self).Diff={})}(this,function(e){"use strict";function t(){}t.prototype={diff:function(u,a,e){var n=2=c&&h<=i+1)return d([{value:this.join(a),count:a.length}]);function o(){for(var e,n=-1*p;n<=p;n+=2){var t=void 0,r=v[n-1],i=v[n+1],o=(i?i.newPos:0)-n;r&&(v[n-1]=void 0);var l=r&&r.newPos+1=c&&h<=o+1)return d(function(e,n,t,r,i){for(var o=0,l=n.length,s=0,u=0;oe.length?t:e}),d.value=e.join(f)):d.value=e.join(t.slice(s,s+d.count)),s+=d.count,d.added||(u+=d.count))}var c=n[l-1];1e.length)&&(n=e.length);for(var t=0,r=new Array(n);t=c.length-2&&u.length<=d.context&&(i=/\n$/.test(a),o=/\n$/.test(f),l=0==u.length&&g.length>r.oldLines,!i&&l&&0e.length)return!1;for(var t=0;t"):i.removed&&t.push(""),t.push((n=i.value,n.replace(/&/g,"&").replace(//g,">").replace(/"/g,"""))),i.added?t.push(""):i.removed&&t.push("")}return t.join("")},e.createPatch=function(e,n,t,r,i,o){return y(e,e,n,t,r,i,o)},e.createTwoFilesPatch=y,e.diffArrays=function(e,n,t){return g.diff(e,n,t)},e.diffChars=function(e,n,t){return r.diff(e,n,t)},e.diffCss=function(e,n,t){return f.diff(e,n,t)},e.diffJson=function(e,n,t){return p.diff(e,n,t)},e.diffLines=L,e.diffSentences=function(e,n,t){return a.diff(e,n,t)},e.diffTrimmedLines=function(e,n,t){var r=i(t,{ignoreWhitespace:!0});return u.diff(e,n,r)},e.diffWords=function(e,n,t){return t=i(t,{ignoreWhitespace:!0}),s.diff(e,n,t)},e.diffWordsWithSpace=function(e,n,t){return s.diff(e,n,t)},e.merge=function(e,n,t){e=b(e,t),n=b(n,t);var r={};(e.index||n.index)&&(r.index=e.index||n.index),(e.newFileName||n.newFileName)&&(F(e)?F(n)?(r.oldFileName=N(r,e.oldFileName,n.oldFileName),r.newFileName=N(r,e.newFileName,n.newFileName),r.oldHeader=N(r,e.oldHeader,n.oldHeader),r.newHeader=N(r,e.newHeader,n.newHeader)):(r.oldFileName=e.oldFileName,r.newFileName=e.newFileName,r.oldHeader=e.oldHeader,r.newHeader=e.newHeader):(r.oldFileName=n.oldFileName||e.oldFileName,r.newFileName=n.newFileName||e.newFileName,r.oldHeader=n.oldHeader||e.oldHeader,r.newHeader=n.newHeader||e.newHeader)),r.hunks=[];for(var i=0,o=0,l=0,s=0;i 2 | 3 | Edit File 4 | 5 |
6 |

{{ file }}

7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 24 | 44 | -------------------------------------------------------------------------------- /vuejspython/builtin/rotate-files.vue: -------------------------------------------------------------------------------- 1 | 16 | 36 | 55 | 56 | -------------------------------------------------------------------------------- /vuejspython/builtin/serve-videos.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 80 | 81 | 134 | -------------------------------------------------------------------------------- /vuejspython/builtin/toggle-exam.vue: -------------------------------------------------------------------------------- 1 | 41 | 110 | 329 | -------------------------------------------------------------------------------- /vuejspython/builtin/view-file.vue: -------------------------------------------------------------------------------- 1 | 10 | 21 | -------------------------------------------------------------------------------- /vuejspython/fsapi.js: -------------------------------------------------------------------------------- 1 | 2 | export class FileSystemAPI { 3 | constructor(baseURL = "") { 4 | this.baseURL = baseURL; 5 | } 6 | 7 | async readFile(filename, prefix="", binary = false) { 8 | const response = await fetch(`${this.baseURL}/${prefix}${filename}`); 9 | if (!response.ok) { 10 | throw new Error(`Failed to read file: ${response.statusText}`); 11 | } 12 | if (binary) { 13 | return response.arrayBuffer(); 14 | } else { 15 | return response.text(); 16 | } 17 | } 18 | 19 | async writeFile(filename, data) { 20 | const response = await fetch(`${this.baseURL}/.file/${filename}`, { 21 | method: "POST", 22 | body: data, 23 | headers: { 24 | "Content-Type": 25 | typeof data === "string" ? "text/plain" : "application/octet-stream", 26 | }, 27 | }); 28 | if (!response.ok) { 29 | throw new Error(`Failed to write file: ${response.statusText}`); 30 | } 31 | return response.text(); 32 | } 33 | 34 | async listFiles() { 35 | const response = await fetch(`${this.baseURL}/.files`); 36 | if (!response.ok) { 37 | throw new Error(`Failed to list files: ${response.statusText}`); 38 | } 39 | return response.json(); 40 | } 41 | 42 | async deleteFile(filename) { 43 | const response = await fetch(`${this.baseURL}/.file/${filename}`, { 44 | method: "DELETE", 45 | }); 46 | if (!response.ok) { 47 | throw new Error(`Failed to delete file: ${response.statusText}`); 48 | } 49 | return response.text(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /vuejspython/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | ___ 11 | 12 | 13 | 14 |
15 | 16 | 17 | 26 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /vuejspython/run.py: -------------------------------------------------------------------------------- 1 | 2 | from fastapi import FastAPI, Response, Request 3 | from fastapi.staticfiles import StaticFiles 4 | from starlette.responses import FileResponse 5 | 6 | import os 7 | import re 8 | import sys 9 | import json 10 | import pathlib 11 | 12 | import click 13 | 14 | @click.command() 15 | @click.argument('vue_file', default='simple.vue') 16 | @click.option('--trust-python', is_flag=True, help='Trust Python code execution') 17 | @click.option('--keep-params', is_flag=True, help='Keep parameters file (.params.json)') 18 | @click.option('--port', default=42042, help='Port to run the server on') 19 | @click.option('--host', default='localhost', help='Host/interface to run the server on') 20 | @click.argument('params', nargs=-1) 21 | def cli(vue_file, trust_python, keep_params, port, host, params): 22 | 23 | if not keep_params: 24 | json.dump([vue_file, *params], open(".params.json", "w")) 25 | 26 | sys.argv = [] 27 | if trust_python: 28 | sys.argv.append('--trust-python') 29 | sys.argv = [*sys.argv, vue_file, *params] 30 | import uvicorn 31 | uvicorn.run('vuejspython.run:startup', reload=True, host=host, port=port) 32 | 33 | if __name__ == "__main__": 34 | cli() 35 | 36 | def startup(): 37 | TRUST_PYTHON = False 38 | if sys.argv[0] == '--trust-python': 39 | TRUST_PYTHON = True 40 | sys.argv = sys.argv[1:] 41 | 42 | if len(sys.argv) == 0: 43 | return 44 | RUNNER_PATH = os.path.realpath(__file__) 45 | RUNNER_DIR = pathlib.Path(os.path.dirname(RUNNER_PATH)) 46 | # if file does not exist AND name starts with :, it is a builtin file 47 | if not os.path.exists(sys.argv[0]) and sys.argv[0].startswith(":"): 48 | BUILTIN = RUNNER_DIR / "builtin" 49 | sys.argv[0] = str(BUILTIN / (sys.argv[0][1:] + ".vue")) 50 | VUE_PATH = os.path.realpath(sys.argv[0]) 51 | VUE_DIR = pathlib.Path(os.path.dirname(VUE_PATH)) 52 | script_python_find = r'' 53 | 54 | # one can hack and redefine this function but anyway if we set --trust-python, everything is open 55 | def is_safe_path(relative_path): 56 | cwd = pathlib.Path.cwd() 57 | path = pathlib.PurePath(relative_path) 58 | joined_path = cwd / path 59 | return joined_path.is_relative_to(cwd) 60 | 61 | def create_parent_dirs(filename): 62 | pathlib.Path(filename).parent.mkdir(parents=True, exist_ok=True) 63 | 64 | app = FastAPI() 65 | 66 | @app.get("/", response_model=None) 67 | async def root(): 68 | return FileResponse(RUNNER_DIR / "index.html", headers=NO_CACHE_HEADERS) 69 | 70 | @app.get("/__entrypoint.vue", response_model=None) 71 | async def entrypoint(): 72 | with open(VUE_PATH) as f: 73 | vue = f.read() 74 | # remove ALL instances of , make sure it is all and multiline 75 | vue = re.sub(script_python_find, "", vue) 76 | return Response(content=vue, media_type="text/plain", headers=NO_CACHE_HEADERS) 77 | 78 | # API endpoint to list files in the cwd 79 | @app.get("/.files", response_model=None) 80 | async def list_files(): 81 | files = [] 82 | # path of all files, recursively 83 | for file in pathlib.Path(".").rglob("*"): 84 | if file.is_file(): 85 | files.append(str(file)) 86 | else: 87 | files.append(str(file) + "/") 88 | return {"files": files} 89 | 90 | # API endpoint to write a file 91 | @app.post("/.file/{filename:path}", response_model=None) 92 | async def write_file(filename: str, request: Request): 93 | if not is_safe_path(filename): 94 | return Response(status_code=400, content="Invalid path (good try!)") 95 | create_parent_dirs(filename) 96 | with open(filename, "wb") as f: 97 | f.write(await request.body()) 98 | return "File written successfully" 99 | 100 | # API endpoint to delete a file 101 | @app.delete("/.file/{filename:path}", response_model=None) 102 | async def delete_file(filename: str): 103 | if not is_safe_path(filename): 104 | return Response(status_code=400, content="Invalid path (good try!)") 105 | pathlib.Path(filename).unlink() 106 | return "File deleted successfully" 107 | 108 | #app.mount("/", StaticFiles(directory='./'), name="static user local") 109 | #StaticFiles.is_not_modified = lambda *args, **kwargs: False 110 | NO_CACHE_HEADERS = { 111 | "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", 112 | "Pragma": "no-cache", 113 | "Expires": "0", 114 | } 115 | class NoCacheStaticFiles(StaticFiles): 116 | def file_response(self, *args, **kwargs) -> Response: 117 | resp = super().file_response(*args, **kwargs) 118 | for k, v in NO_CACHE_HEADERS.items(): 119 | resp.headers.setdefault(k, v) 120 | return resp 121 | app.mount("/.runner", NoCacheStaticFiles(directory=RUNNER_DIR), name="static runner script") 122 | app.mount("/.assets", NoCacheStaticFiles(directory=VUE_DIR), name="static vue assets") 123 | 124 | if TRUST_PYTHON: 125 | with open(VUE_PATH) as f: 126 | vue = f.read() 127 | # all instances of 128 | for m in re.finditer(script_python_find, vue, re.DOTALL): 129 | exec(m.group(1), locals()) # locals is read only (python <=3.12) but still contains app etc, so ok 130 | # need reload in some way? 131 | # need exec at each request? or file change? or only once? i.e. for dev mode... 132 | 133 | @app.get("/{filename:path}") 134 | async def file(filename: str, response: Response): 135 | return FileResponse(filename, headers=NO_CACHE_HEADERS) 136 | 137 | print("###", VUE_DIR, RUNNER_DIR) 138 | 139 | return app 140 | 141 | 142 | if __name__ == "__main__": 143 | exit(cli()) 144 | 145 | --------------------------------------------------------------------------------