├── tests ├── __init__.py ├── README.md ├── static │ └── UseStatic.html ├── test_200_multiple_runs.py ├── test_350_statics.py ├── test_300_templating.py ├── test_900_render.py ├── test_000_json.py ├── conftest.py ├── test_150_cfg.py ├── test_400_emits.py ├── test_800_hook_http.py ├── test_910_one.py ├── test_100_init.py ├── test_700_jscall.py └── test_600_redirect.py ├── android ├── guy.py ├── data │ ├── logo.png │ └── splash.png ├── main.py └── buildozer.spec ├── docs ├── README.md ├── shot_ubuntu.png ├── shot_android10.jpg ├── install.md ├── simplest.md ├── howto_migrate_from_wuy_to_guy.md ├── anatomy.md ├── demo.md ├── howto_build_exe_windows.md ├── multiple.md ├── howto_build_whl_package.md ├── index.md ├── client.md ├── run.md ├── howto_build_apk_android.md └── server.md ├── static ├── logo.png ├── Static.html ├── README.md ├── index.html └── comp.vue ├── testStatic.py ├── testJsInit.py ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── testCam.py ├── .github └── workflows │ └── makedocs.yml ├── testCfg.py ├── testEmit.py ├── testPyInit.py ├── testVueApp.py ├── AEFF.py ├── testPromptForm.py ├── testBeautyUrl.py ├── testTchat.py ├── testRunOnce.py ├── testTapTempo.py ├── testSimple.py ├── mkdocs.yml ├── pyproject.toml ├── testAsync.py ├── testMinipy.py ├── testFetch.py ├── testEelHelloWorld.py ├── testFileUpload.py ├── testRedirect.py ├── testProgress.py ├── .gitignore ├── README.md ├── testPyCallJs.py ├── testAll.py ├── testSudoku.py ├── changelog.md ├── LICENSE └── guy.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/guy.py: -------------------------------------------------------------------------------- 1 | /home/manatlan/Documents/python/guy/guy.py -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | See docs here : https://manatlan.github.io/guy/ 2 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manatlan/guy/HEAD/static/logo.png -------------------------------------------------------------------------------- /docs/shot_ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manatlan/guy/HEAD/docs/shot_ubuntu.png -------------------------------------------------------------------------------- /android/data/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manatlan/guy/HEAD/android/data/logo.png -------------------------------------------------------------------------------- /static/Static.html: -------------------------------------------------------------------------------- 1 | 2 | Hello, I'm from static folder ;-) 3 | -------------------------------------------------------------------------------- /android/data/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manatlan/guy/HEAD/android/data/splash.png -------------------------------------------------------------------------------- /docs/shot_android10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manatlan/guy/HEAD/docs/shot_android10.jpg -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | this folder, and its content is just here for some of the tests. 2 | Specially, testVueApp.py ... it's not needed. 3 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Here are the main tests 2 | (on the features whose be permanents) 3 | 4 | TODO: 5 | 6 | - test guy.fetch more tests ! 7 | - test redefinig guy.FOLDERSTATIC ! 8 | - ... 9 | -------------------------------------------------------------------------------- /tests/static/UseStatic.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /testStatic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy 4 | 5 | class Static(guy.Guy): 6 | size=(200,200) 7 | 8 | if __name__ == "__main__": 9 | Static().run() 10 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 12 | -------------------------------------------------------------------------------- /tests/test_200_multiple_runs.py: -------------------------------------------------------------------------------- 1 | from guy import Guy 2 | 3 | 4 | def test_doubleRun_sameInstance(runner): 5 | class T(Guy): 6 | __doc__="Hello" 7 | def init(self): 8 | self.exit(True) 9 | 10 | t=T() 11 | ok=runner(t) 12 | assert ok 13 | ok=runner(t) 14 | assert ok 15 | 16 | -------------------------------------------------------------------------------- /testJsInit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,asyncio 4 | 5 | class Init(guy.Guy): 6 | size=(200,200) 7 | __doc__=""" 8 | """ 13 | 14 | def display(self,x): 15 | print(x) 16 | 17 | if __name__ == "__main__": 18 | Init().run() 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": false, 3 | "python.testing.pytestEnabled": true, 4 | "python.testing.nosetestsEnabled": false, 5 | "python.testing.unittestEnabled": false, 6 | "python.formatting.provider": "black", 7 | "python.testing.pytestArgs": [ 8 | "tests" 9 | ], 10 | "files.watcherExclude": { 11 | "**/.buildozer/**": true, 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tests/test_350_statics.py: -------------------------------------------------------------------------------- 1 | # import sys; sys.path.insert(0,"..") 2 | from guy import Guy 3 | 4 | 5 | def test_useStatic(runner): 6 | class UseStatic(Guy): 7 | classvar=45 8 | 9 | def __init__(self,v): 10 | self.instancevar=v 11 | Guy.__init__(self) 12 | 13 | def verif(self,a,b): 14 | self.exit(a+b) 15 | 16 | t=UseStatic(42) 17 | total=runner(t) 18 | assert total == 87 -------------------------------------------------------------------------------- /static/comp.vue: -------------------------------------------------------------------------------- 1 | 7 | 17 | 20 | -------------------------------------------------------------------------------- /testCam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,datetime 4 | 5 | class Cam(guy.Guy): 6 | __doc__=""" 7 | 8 | 9 | """ 16 | 17 | 18 | if __name__ == "__main__": 19 | Cam().run() 20 | -------------------------------------------------------------------------------- /.github/workflows/makedocs.yml: -------------------------------------------------------------------------------- 1 | name: Publish mkdocs pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | name: Deploy docs 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout main 13 | uses: actions/checkout@v2 14 | 15 | - name: Deploy docs 16 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | CONFIG_FILE: mkdocs.yml 20 | EXTRA_PACKAGES: build-base 21 | -------------------------------------------------------------------------------- /testCfg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,asyncio,datetime 4 | 5 | class Cfg(guy.Guy): 6 | size=(300,20) 7 | __doc__=""" 8 | 13 | """ 14 | def __init__(self,d): 15 | guy.Guy.__init__(self) 16 | self.d=d 17 | def init(self): 18 | self.cfg.kiki=self.d 19 | 20 | 21 | if __name__ == "__main__": 22 | Cfg( datetime.datetime.now() ).run() 23 | 24 | 25 | -------------------------------------------------------------------------------- /testEmit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,asyncio,datetime 4 | 5 | class Emit(guy.Guy): 6 | size=(300,300) 7 | test="HELLO" 8 | __doc__=""" 9 | <> 10 | 16 | 17 | 18 |
19 | """ 20 | 21 | 22 | if __name__ == "__main__": 23 | Emit( ).run() 24 | 25 | 26 | -------------------------------------------------------------------------------- /testPyInit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,asyncio 4 | 5 | class Init(guy.Guy): 6 | size=(200,200) 7 | __doc__=""" 8 | """ 13 | 14 | def init(self): 15 | 16 | async def periodic(): 17 | while True: 18 | await asyncio.sleep(0.5) 19 | await self.emit("myevent","X") 20 | 21 | asyncio.ensure_future(periodic()) 22 | 23 | if __name__ == "__main__": 24 | Init().run() 25 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | As [Guy is available in Pypi](https://pypi.org/project/guy/), simply : 4 | 5 | ``` 6 | $ python3 -m pip install guy 7 | ``` 8 | Note that [Tornado](https://www.tornadoweb.org) is the unique dependancy. (but you'll need [kivy](https://kivy.org), if you plan to release an android's app) 9 | 10 | 11 | !!! info 12 | The [guy >=0.3.9](https://pypi.org/project/guy/0.3.9/) should fix the [know bug](https://github.com/tornadoweb/tornado/issues/2608) ([py3.8 issue](https://bugs.python.org/issue37373)) , with the use of **Tornado** & **python 3.8** on **Windows 10** platforms ! -------------------------------------------------------------------------------- /tests/test_300_templating.py: -------------------------------------------------------------------------------- 1 | # import sys; sys.path.insert(0,"..") 2 | from guy import Guy 3 | 4 | 5 | def test_templateSubstitution(runner): 6 | class T(Guy): 7 | classvar=45 8 | __doc__=""" 9 | 14 | """ 15 | def __init__(self,v): 16 | self.instancevar=v 17 | Guy.__init__(self) 18 | 19 | def verif(self,a,b): 20 | self.exit(a+b) 21 | t=T(42) 22 | total=runner(t) 23 | assert total == 87 -------------------------------------------------------------------------------- /docs/simplest.md: -------------------------------------------------------------------------------- 1 | # The simplest guy app could look like this 2 | 3 | ```python 4 | #!/usr/bin/python3 -u 5 | from guy import Guy 6 | 7 | class Simple(Guy): 8 | """""" 9 | 10 | def test(self): 11 | print("hello world") 12 | 13 | if __name__ == "__main__": 14 | app=Simple() 15 | app.run() 16 | ``` 17 | 18 | Will run an **app mode**. And can be runned on any OS (android, windows, *nix, mac/iOS, ...) 19 | 20 | 21 | !!! info 22 | If you want to act as a cef instance, replace `app.run()` by `app.runCef()` 23 | 24 | If you want to act as a http server, replace `app.run()` by `app.serve()` 25 | -------------------------------------------------------------------------------- /testVueApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | import guy,os 3 | import vbuild # vbuild>=0.8.1 !!!!!! 4 | 5 | class VueApp(guy.Guy): 6 | size=(400,200) 7 | 8 | def render(self,path): #here is the magic 9 | # this method is overrided, so you can render what you want 10 | # load your template (from static folder) 11 | with open( os.path.join(path,"static/index.html") ) as fid: 12 | content=fid.read() 13 | 14 | # load all vue/sfc components 15 | v=vbuild.render( os.path.join(path,"static/*.vue") ) 16 | 17 | # and inject them in your template 18 | return content.replace("",str(v)) 19 | 20 | if __name__=="__main__": 21 | VueApp().run() 22 | -------------------------------------------------------------------------------- /AEFF.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import guy 3 | 4 | class Prompt(guy.Guy): 5 | """ 6 | 7 | 8 | <> ? 9 | <form onsubmit="self.post( this.txt.value ); return false"> 10 | <input name="txt" value="<<value>>"/> 11 | <input type="submit" value="ok"/> 12 | </form> 13 | """ 14 | size=(300,200) 15 | 16 | def __init__(self,title,value=""): 17 | self.title=title 18 | self.value=value 19 | super().__init__() 20 | 21 | def post(self,value): 22 | if value.strip(): 23 | self.exit(value.strip()) 24 | 25 | if __name__=="__main__": 26 | app=Prompt("name","yolo") 27 | print(app.run()) 28 | -------------------------------------------------------------------------------- /testPromptForm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import guy 3 | 4 | class Prompt(guy.Guy): 5 | """ 6 | <style>body {background:#EEE}</style> 7 | 8 | <<title>> ? 9 | <form onsubmit="self.post( this.txt.value ); return false"> 10 | <input name="txt" value="<<value>>"/> 11 | <input type="submit" value="ok"/> 12 | </form> 13 | """ 14 | size=(300,200) 15 | 16 | def __init__(self,title,value=""): 17 | self.title=title 18 | self.value=value 19 | super().__init__() 20 | 21 | def post(self,value): 22 | if value.strip(): 23 | self.exit(value.strip()) 24 | 25 | if __name__=="__main__": 26 | app=Prompt("name","yolo") 27 | print(app.run()) 28 | -------------------------------------------------------------------------------- /testBeautyUrl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | import guy 3 | 4 | @guy.http(r"/item/(\d+)") 5 | def getItem(web,number): 6 | return Win(number) 7 | 8 | class Win(guy.Guy): 9 | """ 10 | Hello <<info>> 11 | 12 | <button onclick="self.test()">Test</button> 13 | """ 14 | def __init__(self,q): 15 | self.info=q 16 | super().__init__() 17 | 18 | def test(self): 19 | return dict(script=""" 20 | document.body.innerHTML+="ok"; 21 | """) 22 | 23 | 24 | class App(guy.Guy): 25 | """ 26 | <a href="/item/42">Via HTTP hook</a> 27 | <a href="/Win?q=43">classic redirection</a> 28 | """ 29 | 30 | 31 | 32 | if __name__ == "__main__": 33 | app=App() 34 | app.serve() 35 | -------------------------------------------------------------------------------- /tests/test_900_render.py: -------------------------------------------------------------------------------- 1 | from guy import Guy,http 2 | 3 | 4 | 5 | def test_render(runner): 6 | class T(Guy): 7 | __doc__="""NOT USED""" 8 | 9 | async def init(self): 10 | self.retour =await self.js.end() 11 | 12 | def render(self,path): 13 | return """ 14 | <script src="guy.js"></script> 15 | <script> 16 | function end() { 17 | if(self.render === undefined) // render is not availaible as rpc js method ! 18 | self.end("ok") 19 | } 20 | </script> 21 | """ 22 | 23 | def end(self,txt): 24 | self.exit(txt) 25 | 26 | t=T() 27 | txt=runner(t) 28 | assert txt=="ok" 29 | 30 | -------------------------------------------------------------------------------- /testTchat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,asyncio 4 | 5 | class Tchat(guy.Guy): 6 | __doc__=""" 7 | <form onsubmit="guy.emit( 'evt-send-txt', this.txt.value ); return false"> 8 | <input id="txt" value=""/> 9 | <input type="submit" value="ok"/> 10 | </form> 11 | 12 | <script> 13 | guy.on( "evt-send-txt", function(txt) { 14 | document.body.innerHTML+=`<li>${txt}</li>`; 15 | document.querySelector("#txt").focus(); 16 | }) 17 | </script> 18 | 19 | <span style="color:yellow;background:red;padding:4;border:2px solid yellow;position:fixed;top:20px;right:20px;transform: rotate(10deg);"> 20 | Open a second tab (better: from another computer)<br/> 21 | to see this simple tchat on ;-) 22 | </span>""" 23 | 24 | 25 | if __name__ == "__main__": 26 | Tchat().serve() 27 | -------------------------------------------------------------------------------- /tests/test_000_json.py: -------------------------------------------------------------------------------- 1 | 2 | import guy 3 | import asyncio 4 | from datetime import date, datetime 5 | 6 | def test_json(): 7 | def test(j, testType=None): 8 | def testSUS(obj, testType=None): 9 | s = guy.jDumps(obj) 10 | nobj = guy.jLoads(s) 11 | assert type(nobj) == testType 12 | 13 | testSUS(dict(v=j), dict) 14 | testSUS([j, dict(a=[j])], list) 15 | testSUS(j, testType) 16 | 17 | class Ob: 18 | def __init__(self): 19 | self.name = "koko" 20 | 21 | test(datetime.now(), datetime) 22 | test(date(1983, 5, 20), datetime) 23 | test(b"kkk", str) 24 | test("kkk", str) 25 | test(42, int) 26 | test(4.2, float) 27 | test(None, type(None)) 28 | test(Ob(), dict) 29 | test(datetime.now() - datetime.now(), str) 30 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest,time,asyncio 2 | 3 | # @pytest.fixture(params=["run","runCef","serve"]) 4 | # @pytest.fixture(params=["run","runCef"]) 5 | @pytest.fixture(params=["run"]) 6 | # @pytest.fixture(params=["serve"]) 7 | # @pytest.fixture(params=["runCef"]) 8 | def runner(request): 9 | def _( ga, **kargs ): 10 | time.sleep(0.5) # leave the time to shutdown previous instance 11 | if request.param=="serve": 12 | return getattr(ga,request.param)(port=10000) 13 | else: 14 | return getattr(ga,request.param)(**kargs) 15 | 16 | return _ 17 | 18 | # @pytest.yield_fixture(scope='session') 19 | # def event_loop(request): 20 | # """Create an instance of the default event loop for each test case.""" 21 | # loop = asyncio.get_event_loop_policy().new_event_loop() 22 | # yield loop 23 | # loop.close() -------------------------------------------------------------------------------- /testRunOnce.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from guy import Guy 4 | from datetime import datetime 5 | 6 | class Simple(Guy): 7 | size=(400,200) 8 | __doc__=""" 9 | 10 | <script> 11 | function set() { 12 | localStorage["hello"]=new Date(); 13 | init() 14 | } 15 | 16 | function init() { 17 | document.body.innerHTML += (localStorage["hello"] || "Empty"); 18 | } 19 | 20 | </script> 21 | 22 | <button onclick="set()">set</button> 23 | 24 | <span style="color:yellow;background:red;padding:4;border:2px solid yellow;position:fixed;top:20px;right:20px;transform: rotate(10deg);"> 25 | If you try to run a second one<br/> 26 | It will focus to this one !<br/> 27 | (and keep storage !) 28 | </span> 29 | """ 30 | async def init(self): 31 | await self.js.init() 32 | 33 | if __name__ == "__main__": 34 | x=Simple() 35 | x.run(one=True) # 11:21 36 | -------------------------------------------------------------------------------- /testTapTempo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,datetime 4 | 5 | class TapTempo(guy.Guy): 6 | __doc__=""" 7 | <style> 8 | body {background: yellow} 9 | #tempo {font-size:3em} 10 | </style> 11 | <button style="float:right;font-size:2em" onclick="self.exit()">X</button> 12 | <script> 13 | function tap() { 14 | self.tap().then(x=>{document.querySelector('#tempo').innerHTML=x}) 15 | } 16 | document.onclick=tap 17 | </script> 18 | <center>Tap Tempo!</center> 19 | <center id="tempo"></center> 20 | """ 21 | size=(140,100) 22 | t=[] 23 | 24 | def tap(self): 25 | self.t.append( datetime.datetime.now() ) 26 | ll=[ (j-i).microseconds for i, j in zip(self.t[:-1], self.t[1:]) ][-5:] 27 | if ll: 28 | return int(60000000*len(ll)/sum(ll)) 29 | 30 | if __name__ == "__main__": 31 | TapTempo().run() 32 | -------------------------------------------------------------------------------- /testSimple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from guy import Guy 4 | from datetime import datetime 5 | 6 | class Simple(Guy): 7 | #~ size=FULLSCREEN 8 | size=(200,400) 9 | __doc__=""" 10 | <style>body {margin:0px; padding:5px; border: 1px solid black}</style> 11 | <script> 12 | async function test() { 13 | var cpt=await guy.cfg.cpt || 0; 14 | var x=await self.getTimeStamp() 15 | document.querySelector("body").innerHTML += "<li>"+cpt+'/'+x+"</li>"; 16 | guy.cfg.cpt=cpt+1 17 | } 18 | 19 | guy.init( test ) 20 | </script> 21 | 22 | <img src="logo.png" width=42> 23 | <button onclick="test()">test</button> 24 | <button style="float:right;font-size:2em" onclick="self.exit()">X</button> 25 | <hr/> 26 | """ 27 | 28 | def getTimeStamp(self): 29 | return datetime.now() 30 | 31 | if __name__ == "__main__": 32 | x=Simple() 33 | x.run() 34 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Guy's Docs 2 | site_description: Simple GUI (using chrome) for python 3 | 4 | theme: 5 | name: 'material' 6 | palette: 7 | primary: 'purple' 8 | accent: 'purple' 9 | 10 | repo_name: manatlan/guy 11 | repo_url: https://github.com/manatlan/guy 12 | edit_uri: "" 13 | 14 | nav: 15 | - Introduction: 'index.md' 16 | - Example: 'simplest.md' 17 | - Install: 'install.md' 18 | - Anatomy: anatomy.md 19 | - Server side: server.md 20 | - Client side: client.md 21 | - Multiple Instance: multiple.md 22 | - Run guy's app: run.md 23 | - Demo: demo.md 24 | - How-to: 25 | - From wuy to guy: howto_migrate_from_wuy_to_guy.md 26 | - Freeze exe/windows: howto_build_exe_windows.md 27 | - Release an APK/android: howto_build_apk_android.md 28 | - Release a pypi/whl/package: howto_build_whl_package.md 29 | markdown_extensions: 30 | - markdown.extensions.codehilite: 31 | guess_lang: false 32 | - admonition -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "guy" 3 | version = "0.7.6" 4 | description = "A simple module for making HTML GUI applications with python3" 5 | authors = ["manatlan <manatlan@gmail.com>"] 6 | readme = 'README.md' 7 | license="Apache-2.0" 8 | keywords=['gui', 'html', 'javascript', 'electron', "asyncio", "tornado", "websocket"] 9 | homepage = "https://github.com/manatlan/guy" 10 | repository = "https://github.com/manatlan/guy" 11 | documentation = "https://github.com/manatlan/guy" 12 | classifiers = [ 13 | "Operating System :: OS Independent", 14 | "Topic :: Software Development :: Libraries :: Python Modules", 15 | "Topic :: Software Development :: Build Tools", 16 | "License :: OSI Approved :: Apache Software License", 17 | ] 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.5" 21 | tornado = "^6.0" 22 | 23 | [tool.poetry.dev-dependencies] 24 | pytest = "^3.0" 25 | pytest-cov = "^2.6" 26 | cefpython3 = "^66.0" 27 | vbuild = "0.8.1" 28 | 29 | [build-system] 30 | requires = ["poetry>=0.12"] 31 | build-backend = "poetry.masonry.api" 32 | -------------------------------------------------------------------------------- /testAsync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,asyncio,time 4 | 5 | class asyncTest(guy.Guy): 6 | __doc__=""" 7 | <style> body.wsguy {background:yellow} </style> 8 | 9 | <script> 10 | function rep(x) { 11 | document.getElementById("rep").innerHTML="<li>"+x+"</li>"+document.getElementById("rep").innerHTML; 12 | } 13 | </script> 14 | 15 | <button onclick="self.doSyncQuick().then(rep)">sync quick</button> 16 | <button onclick="self.doSyncLong().then(rep)">sync long (block ui)</button> 17 | <button onclick="self.doASyncLong().then(rep)">async long</button> 18 | 19 | <div id="rep"></div> 20 | """ 21 | size=(200,200) 22 | 23 | def doSyncQuick(self): 24 | return "quick" 25 | 26 | def doSyncLong(self): # run synchro (hangs the ui) 27 | time.sleep(3) 28 | return "long" 29 | 30 | async def doASyncLong(self): # run asynchro !!! (it doesn't hang the UI !) 31 | await asyncio.sleep(3) 32 | return "async long" 33 | 34 | if __name__=="__main__": 35 | app=asyncTest() 36 | app.run() 37 | -------------------------------------------------------------------------------- /testMinipy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from guy import Guy,FULLSCREEN 4 | import asyncio,datetime 5 | 6 | class Minipy(Guy): 7 | size=(300,200) 8 | txt="3 * '#'" 9 | __doc__=""" 10 | <style> 11 | div#back form {display:inline} 12 | </style> 13 | <script> 14 | async function test(txt) { 15 | document.querySelector("#r").innerHTML = await self.post( txt ) 16 | } 17 | </script> 18 | <div id='back'> 19 | <h3>minipy</h3> 20 | <form onsubmit="test(this.txt.value); return false"> 21 | <input id='n' name="txt" value="<<txt>>" onfocus="var val=this.value; this.value=''; this.value= val;" /> 22 | <button> run </button> 23 | </form> 24 | <button onclick="self.exit()">exit</button> 25 | </div> 26 | <script> 27 | document.querySelector("#n").focus() 28 | </script> 29 | <div id=r></div>""" 30 | 31 | def post(self,txt): 32 | try: 33 | return eval(txt, globals(), locals()) 34 | except Exception as e: 35 | return "error:%s"%e 36 | 37 | 38 | if __name__ == "__main__": 39 | x=Minipy() 40 | x.run(log=True) 41 | -------------------------------------------------------------------------------- /docs/howto_migrate_from_wuy_to_guy.md: -------------------------------------------------------------------------------- 1 | # How to migrate from wuy 2 | 3 | (**wuy** is the ancestor of **guy**) 4 | 5 | - Replace all `wuy` keyword in your py files, by `guy` 6 | - Replace all `wuy` in html/js files, by `guy` (for core methods) or `self` (for those which you have declared in your class) ... see ([client side](client.md)) 7 | - Replace `wuy.Window`/`wuy.Server` by `guy.Guy` 8 | - `.get() & .set()` configs are replaced by `self.cfg` (py side) and `guy.cfg` (js side) 9 | - Rename your `web` folder to `static` folder, if needed. 10 | - At launch, get the instance, and apply one of theses methods: 11 | - instance.run() : for classical "app mode" 12 | - instance.serve() : for classical "server mode" 13 | 14 | From wuy: 15 | 16 | ```python 17 | AppWindow() 18 | ``` 19 | 20 | to guy: 21 | 22 | ```python 23 | app=AppWindow() 24 | app.run() 25 | ``` 26 | 27 | !!! info 28 | Here is my biggest [wuy's app migration to guy](https://github.com/manatlan/jbrout3/commit/17ca6f5054f04de88af2ffdf27468f4c48ee9725) 29 | 30 | !!! info 31 | if socket close : client will reconnect ! (it will not close the app, like **wuy** did) -------------------------------------------------------------------------------- /tests/test_150_cfg.py: -------------------------------------------------------------------------------- 1 | from guy import Guy,http 2 | import os 3 | 4 | def test_cfg(runner): 5 | class T(Guy): 6 | """ 7 | <script> 8 | 9 | async function cadd() { 10 | guy.cfg.value = (await guy.cfg.value) +'(client)'; 11 | } 12 | 13 | async function stop() { 14 | let c=await guy.cfg.value; 15 | let unknown = await guy.cfg.unknown 16 | if(unknown) c+=unknown 17 | self.stop(c) 18 | } 19 | 20 | </script> 21 | 22 | """ 23 | def __init__(self): 24 | Guy.__init__(self) 25 | self.cfg.value = "(__init__)" 26 | 27 | async def init(self): 28 | self.cfg.value += "(init)" 29 | await self.js.cadd() 30 | await self.js.stop() 31 | 32 | def stop(self,c): 33 | ccfg=c 34 | scfg=self.cfg.value 35 | if self.cfg.unknown: scfg+=self.cfg.unknown 36 | 37 | self.exit(ccfg == scfg) 38 | 39 | t=T() 40 | ok=runner(t) 41 | if t.cfg._file and os.path.isfile(t.cfg._file): os.unlink(t.cfg._file) 42 | assert ok 43 | -------------------------------------------------------------------------------- /tests/test_400_emits.py: -------------------------------------------------------------------------------- 1 | from guy import Guy 2 | 3 | def test_emits(runner): 4 | class T(Guy): 5 | __doc__=""" 6 | <script> 7 | var word=""; 8 | 9 | guy.on( "hello", async function(letter) { 10 | word+=letter; 11 | }) 12 | 13 | guy.on( "end", async function() { 14 | await self.endtest(word) 15 | }) 16 | 17 | guy.init( async function() { 18 | guy.emitMe("hello","B") // avoid socket, but it counts 19 | guy.emit("hello","C") // emit all clients 20 | await self.makeEmits() // generate server emits 21 | }) 22 | 23 | guy.emitMe("hello","A") // avoid socket, so can be run before init 24 | 25 | </script> 26 | """ 27 | async def makeEmits(self): 28 | await self.emit("hello","D") # emit all clients 29 | await self.emitMe("hello","E") # emit ME only 30 | await self.emitMe("end") # emit ME only and finnish the test 31 | def endtest(self,word): 32 | self.exit(word) 33 | t=T() 34 | word=runner(t) 35 | assert word=="ABCDE" 36 | -------------------------------------------------------------------------------- /tests/test_800_hook_http.py: -------------------------------------------------------------------------------- 1 | from guy import Guy,http 2 | 3 | @http(r"/item/(\d+)") 4 | def getItem(web,number): 5 | web.write( "item %s"%number ) 6 | 7 | 8 | def test_hook_with_classic_fetch(runner): 9 | class T(Guy): 10 | __doc__="""Hello 11 | <script> 12 | async function testHook() { 13 | var r=await window.fetch("/item/42") 14 | return await r.text() 15 | } 16 | </script> 17 | """ 18 | async def init(self): 19 | retour =await self.js.testHook() 20 | self.exit(retour) 21 | 22 | t=T() 23 | retour=runner(t) 24 | assert retour == "item 42" 25 | 26 | 27 | 28 | def test_hook_with_guy_fetch(runner): 29 | class T(Guy): 30 | __doc__="""Hello 31 | <script> 32 | async function testHook() { 33 | var r=await guy.fetch("/item/42") // not needed in that case (no cors trouble!) 34 | return await r.text() 35 | } 36 | </script> 37 | """ 38 | async def init(self): 39 | retour =await self.js.testHook() 40 | self.exit(retour) 41 | 42 | t=T() 43 | retour=runner(t) 44 | assert retour == "item 42" 45 | 46 | -------------------------------------------------------------------------------- /docs/anatomy.md: -------------------------------------------------------------------------------- 1 | # Anatomy : how it works 2 | 3 | A guy's app can be seen as a single application. It could be true for `app` & `cef` mode. 4 | 5 | Under the hood : it's basically 2 things: 6 | 7 | * [Server Side](server.md) : An http & socket server 8 | * [Client Side](client.md) : A javascript lib which make the glue with the server. 9 | 10 | For `app` & `cef` mode : guy run the two in a windowed app. (there is one server & one client) 11 | 12 | For `server` mode : guy run the server, and a classical browser can be a client, when connected. (there is one server & many clients) 13 | 14 | In all cases : the http server serve the client as a html component. And the client communicate with the server with a websocket. 15 | 16 | !!! info 17 | Although there is always a http server running, under the hood. Only the one in server mode is listening wide (0.0.0.0) to accept connections 18 | from all the world ;-). Thoses in app/cef mode are listening on localhost only (can't accept connections from another computer) 19 | 20 | Technically : it's the marvellous [tornado](https://www.tornadoweb.org/en/stable/), an asynchronous networking library which handle http & socket. So **guy** can work for python >= 3.5 (ready for raspberry pi !) -------------------------------------------------------------------------------- /testFetch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # # -*- coding: utf-8 -*- 3 | import guy 4 | 5 | # call a http service during an async rpc method call 6 | 7 | class Fetch(guy.Guy): # name the class as the web/<class_name>.html 8 | size=guy.FULLSCREEN 9 | __doc__=""" 10 | <style> 11 | body,html,center {width:100%;height:100%;margin:0px;padding:0px;cursor:pointer;background:black} 12 | img { 13 | max-height: 100%; 14 | width: auto; 15 | } 16 | div {position:fixed;top:10px;right:20px;z-index:2;color:red;font-size:100px;font-family:sans-serif} 17 | </style> 18 | <script> 19 | var list=[]; 20 | 21 | guy.init( function() { 22 | 23 | guy.fetch("https://www.reddit.com/r/pics/.rss") // not possible with classic window.fetch() 24 | .then( x=>{return x.text()} ) 25 | .then( x=>{ 26 | list=x.match(/https:..i\.redd\.it\/[^\.]+\..../g) 27 | change() 28 | }) 29 | 30 | }) 31 | 32 | function change(n) { 33 | document.querySelector("#i").src=list[0]; 34 | list.push( list.shift() ) 35 | } 36 | </script> 37 | <center> 38 | <img id="i" src="" onclick="change()"/> 39 | </center> 40 | <div onclick="guy.exit()">X</div> 41 | """ 42 | 43 | 44 | if __name__=="__main__": 45 | Fetch().run() 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build dist for pypi", 8 | "type": "shell", 9 | "command": [ 10 | "poetry build", 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | }, 16 | "problemMatcher": [], 17 | "presentation": { 18 | "panel": "new", 19 | "focus": true 20 | } 21 | }, 22 | { 23 | "label": "upload to pypi", 24 | "type": "shell", 25 | "command": "poetry publish", 26 | "problemMatcher": [], 27 | "presentation": { 28 | "panel": "new", 29 | "focus": true 30 | } 31 | }, 32 | 33 | 34 | // { 35 | // "label": "Tests", 36 | // "type": "shell", 37 | // "command": "tox", 38 | // "problemMatcher": [], 39 | // "presentation": { 40 | // "panel": "new", 41 | // "focus": true 42 | // } 43 | // } 44 | ] 45 | } -------------------------------------------------------------------------------- /testEelHelloWorld.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | # -*- coding: utf-8 -*- 3 | import guy 4 | 5 | """ 6 | The python eel "hello world" in a guy's app 7 | (https://github.com/samuelhwilliams/Eel/tree/master/examples/01%20-%20hello_world) 8 | 9 | NEED GUY >= 0.4.3 10 | """ 11 | 12 | class Hello(guy.Guy): 13 | """ 14 | <script> 15 | guy.init( async function() { // ensure that everything is started/connected (socket cnx) 16 | say_hello_js( "Javascript World!" ) // say local hello from js world 17 | await self.say_hello_py("Javascript World!") // say python/distant hello from js world (await not needed in this case) 18 | }) 19 | 20 | function say_hello_js(x) { // declare a local method to print txt, js side 21 | document.body.innerHTML+= `Hello from ${x}<br>`; 22 | } 23 | </script> 24 | """ 25 | size=(300, 200) # set the size of the client window 26 | 27 | async def init(self): # everything is started, we start the process 28 | self.say_hello_py("Python World!") # say local hello from pyworld 29 | await self.js.say_hello_js("Python World!") # say js/distant hello from pyworld 30 | 31 | def say_hello_py(self,x): # declare the python method, to be seen/used on js side. 32 | print('Hello from %s' % x) 33 | 34 | if __name__ == "__main__": 35 | Hello().run(log=True) -------------------------------------------------------------------------------- /tests/test_910_one.py: -------------------------------------------------------------------------------- 1 | from guy import Guy,http 2 | 3 | 4 | class T(Guy): 5 | __doc__="""<script> 6 | async function storage(mode) { 7 | switch(mode) { 8 | case "get": 9 | return localStorage["var"]==42; 10 | case "set": 11 | localStorage["var"]=42; 12 | return true 13 | default: 14 | alert("mode='"+mode+"' ?!?") 15 | } 16 | 17 | } 18 | </script>""" 19 | size=(100,100) 20 | 21 | def __init__(self,mode): 22 | self.mode=mode 23 | super().__init__() 24 | 25 | async def init(self): 26 | ok =await self.js.storage(self.mode) 27 | self.exit(ok) 28 | 29 | def test_no_lockPort(runner): 30 | t=T("get") 31 | ok=runner(t) 32 | assert not ok,"localStorage is already present ?!" 33 | 34 | t=T("set") 35 | ok=runner(t) 36 | assert ok,"setting localstorage not possible ?!" 37 | 38 | t=T("get") 39 | ok=runner(t) 40 | assert not ok,"win has memory ;-(" 41 | 42 | # CAN't WORK IN pytest, as is 43 | 44 | # def test_lockPort(): # app mode only (broken with cef ... coz ioloop/pytests) 45 | # t=T("set") 46 | # ok=t.runCef(one=True) 47 | # assert ok==True 48 | 49 | # t=T("get") 50 | # ok=t.runCef(one=True) 51 | # assert ok==True # localStorage is persistent ! 52 | -------------------------------------------------------------------------------- /tests/test_100_init.py: -------------------------------------------------------------------------------- 1 | from guy import Guy 2 | 3 | 4 | def test_init(runner): 5 | class T(Guy): 6 | __doc__=""" 7 | <script> 8 | guy.init( async function() { 9 | await self.append("C") 10 | await self.end() 11 | }) 12 | </script> 13 | """ 14 | def __init__(self): 15 | Guy.__init__(self) 16 | self.word=["A"] 17 | def init(self): 18 | self.append("B") 19 | 20 | def append(self,letter): 21 | self.word.append(letter) 22 | 23 | def end(self): 24 | self.exit(self.word) 25 | t=T() 26 | ll=runner(t) 27 | assert ll==list("ABC") 28 | 29 | 30 | def test_init_async(runner): 31 | class T(Guy): 32 | __doc__=""" 33 | <script> 34 | guy.init( async function() { 35 | await self.append("C") 36 | await self.end() 37 | }) 38 | </script> 39 | """ 40 | def __init__(self): 41 | Guy.__init__(self) 42 | self.word=["A"] 43 | 44 | async def init(self): 45 | self.append("B") 46 | 47 | def append(self,letter): 48 | self.word.append(letter) 49 | 50 | def end(self): 51 | self.exit(self.word) 52 | 53 | t=T() 54 | ll=runner(t) 55 | assert ll==list("ABC") 56 | -------------------------------------------------------------------------------- /docs/demo.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | ## Mode Server 4 | 5 | Server's mode is more prompt for demo (Here on [glitch.com](https://glitch.com)): 6 | 7 | - [a simple guy's app](https://starter-guy.glitch.me/#/) ([Play with sources](https://glitch.com/edit/#!/starter-guy)) 8 | - [a guy's app serving a vuejs/sfc UI](https://starter-guy-vuejs.glitch.me/#/) ([Play with sources](https://glitch.com/edit/#!/starter-guy-vuejs)) 9 | 10 | 11 | ## Mode App/cef 12 | If you want to try app/cef mode, just test the [test*.py files here](https://github.com/manatlan/guy) on your computer. 13 | 14 | Here is one : 15 | <p align="center"> 16 | <table> 17 | <tr> 18 | <td valign="top"> 19 | On Ubuntu<br> 20 | <img src="https://manatlan.github.io/guy/shot_ubuntu.png" width="300" border="1" style="border:1px solid black"/> </td> 21 | <td valign="top"> 22 | On Android10<br> 23 | <img src="https://manatlan.github.io/guy/shot_android10.jpg" width="300" border="1" style="border:1px solid black"/> 24 | </td> 25 | </tr> 26 | </table> 27 | </p> 28 | 29 | ## Mode App on android 30 | 31 | You can try the [bubble killer's game sources](https://github.com/manatlan/guy-bubble-killer-apk) ( [here is on the playstore](https://play.google.com/store/apps/details?id=com.manatlan.guy.bubblekiller) ) 32 | 33 | Or, the less interesting [howto demo apk, on the playstore](https://play.google.com/store/apps/details?id=demo.com.guy) 34 | -------------------------------------------------------------------------------- /testFileUpload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | # -*- coding: utf-8 -*- 3 | import guy 4 | 5 | 6 | class FileUpload(guy.Guy): 7 | """ 8 | <style> 9 | div#drop { 10 | border:2px dotted green; 11 | padding:10px; 12 | } 13 | </style> 14 | <body oncontextmenu="return false"> 15 | <hr> 16 | <input type="file" onchange="upload(this.files[0])"/> 17 | <hr> 18 | <div id="drop" ondrop="drop(event)" ondragover="allow(event)">drop files here</div> 19 | <hr> 20 | 21 | </body> 22 | <script> 23 | 24 | function allow(e) { 25 | e.preventDefault(); 26 | e.dataTransfer.dropEffect = 'copy'; 27 | } 28 | 29 | function drop(e) { 30 | if(e.dataTransfer.files) upload(e.dataTransfer.files[0]) 31 | e.preventDefault(); 32 | } 33 | 34 | function upload(file) { 35 | let reader = new FileReader(); 36 | //reader.readAsText(file, "UTF-8"); 37 | reader.readAsBinaryString(file); 38 | reader.onload = async function (evt) { 39 | await self.upload( file.name, evt.target.result ) 40 | } 41 | } 42 | 43 | function add(txt) { 44 | document.body.innerHTML+=`<li>${txt}</li>`; 45 | } 46 | 47 | </script> 48 | """ 49 | size=(300,300) 50 | 51 | 52 | async def upload(self, name,content): 53 | await self.js.add(name+" (%s)"%len(content)) 54 | 55 | if __name__=="__main__": 56 | FileUpload().run() 57 | -------------------------------------------------------------------------------- /testRedirect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | # -*- coding: utf-8 -*- 3 | from guy import Guy 4 | 5 | class Glob: 6 | i=0 7 | def test(self): 8 | Glob.i+=1 9 | return Glob.i 10 | 11 | #========================================== 12 | class Marco(Guy,Glob): 13 | #========================================== 14 | """ Hello Marco 15 | 16 | <script> 17 | async function aff( am ) {document.body.innerHTML+=await am();} 18 | </script> 19 | 20 | <button onclick="aff( self.t1 )">t1</button> 21 | <button onclick="aff( self.test )">Test</button> 22 | 23 | 24 | <a href="/Polo">go to polo</a> 25 | <a href="/nowhere">go to nowhere</a> 26 | <a href="/logo.png">go to logo.png</a> 27 | 28 | """ 29 | 30 | def init(self): 31 | print("Start Marco") 32 | 33 | def t1(self): 34 | return "t1" 35 | 36 | 37 | #========================================== 38 | class Polo(Guy,Glob): 39 | #========================================== 40 | """ Hello Polo 41 | 42 | <script> 43 | async function aff( am ) {document.body.innerHTML+=await am();} 44 | </script> 45 | 46 | <button onclick="aff( self.t2 )">t2</button> 47 | <button onclick="aff( self.test )">Test</button> 48 | 49 | <a href="/">go to marco</a> 50 | """ 51 | 52 | def init(self): 53 | print("Start Polo") 54 | 55 | def t2(self): 56 | return "t2" 57 | 58 | 59 | if __name__ == "__main__": 60 | app=Marco() 61 | app.run(log=True) 62 | #~ app.run() 63 | 64 | -------------------------------------------------------------------------------- /testProgress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import guy,asyncio 4 | 5 | class progress(guy.Guy): # name the class as the web/<class_name>.html 6 | __doc__=""" 7 | <style> 8 | body {background: #EEE} 9 | .pb { 10 | border:1px solid black; 11 | background:white; 12 | } 13 | .pb div { 14 | background:blue; 15 | height:20%; 16 | width:0px; 17 | } 18 | </style> 19 | 20 | 21 | <button onclick="run(this,'p1',0.1)">Go</button> 22 | <div class="pb" id="p1"><div></div></div> 23 | 24 | <button onclick="run(this,'p2',0.02)">Go</button> 25 | <div class="pb" id="p2"><div></div></div> 26 | 27 | <script> 28 | async function run(b,pb,speed) { 29 | b.disabled=true; 30 | let m=await self.doTheJob(pb,speed) 31 | b.disabled=false; 32 | console.log(m); 33 | } 34 | 35 | function syncProgressBar( pb,percent ) { 36 | document.querySelector( `#${pb} div` ).style.width=percent+"%"; 37 | } 38 | </script> 39 | 40 | 41 | <span style="color:yellow;background:red;padding:4;border:2px solid yellow;position:fixed;top:20px;right:20px;transform: rotate(10deg);"> 42 | Run in a second tab, to see<br/> 43 | that it's isolated by instance ! 44 | </span> 45 | 46 | """ 47 | async def doTheJob(self,pb,speed): 48 | for i in range(101): 49 | await asyncio.sleep(speed) # simulate the job 50 | await self.js.syncProgressBar(pb,i) 51 | return "Job Done %s!" % pb 52 | 53 | 54 | if __name__=="__main__": 55 | d=progress() 56 | d.serve() 57 | -------------------------------------------------------------------------------- /tests/test_700_jscall.py: -------------------------------------------------------------------------------- 1 | 2 | from guy import Guy,JSException 3 | 4 | def test_jscall(runner): 5 | class W1(Guy): 6 | __doc__=""" 7 | <script> 8 | var ll=[]; 9 | 10 | function adds(a,b) { 11 | ll.push(a) 12 | ll.push(b) 13 | return a+b 14 | } 15 | 16 | function makeAnError() { 17 | callInError(); // raise an exception on js side 18 | } 19 | 20 | async function ASyncAdds(a,b) { 21 | ll.push(a) 22 | ll.push(b) 23 | return a+b 24 | } 25 | 26 | guy.init( async function() { 27 | await new Promise(r => setTimeout(r, 100)); // wait, to be sure that init() is called before step1() 28 | await self.step1() 29 | await self.step2() 30 | self.stop( ll ) 31 | }) 32 | 33 | </script> 34 | """ 35 | ll=[] 36 | 37 | async def init(self): 38 | self.ll.append( await self.js.adds("A","B") ) 39 | 40 | async def step1(self): 41 | self.ll.append( await self.js.adds("C","D") ) 42 | self.ll.append( await self.js.ASyncAdds("E","F") ) 43 | 44 | async def step2(self): 45 | try: 46 | await self.js.UNKNOWNMETHOD("C","D") 47 | except JSException: 48 | self.ll.append("Unknown") 49 | try: 50 | await self.js.makeAnError() 51 | except JSException: 52 | self.ll.append("Error") 53 | 54 | def stop(self,jll): 55 | assert jll==['A', 'B', 'C', 'D', 'E', 'F'] 56 | assert self.ll==['AB', 'CD', 'EF', 'Unknown', 'Error'] 57 | self.exit(True) 58 | 59 | t=W1() 60 | ok=runner(t) 61 | assert ok 62 | -------------------------------------------------------------------------------- /tests/test_600_redirect.py: -------------------------------------------------------------------------------- 1 | 2 | from guy import Guy 3 | 4 | def test_redirect(runner): 5 | class W1(Guy): 6 | __doc__=""" 7 | <script> 8 | guy.init( async function() { 9 | if(document.location.href.indexOf("#W1")>=0) { // first call 10 | if(await self.step1()) 11 | document.location.href="/W2?param=42" 12 | } 13 | else 14 | self.step3() 15 | }) 16 | </script> 17 | 1 18 | """ 19 | def step1(self): 20 | return True 21 | def step3(self): 22 | self.exit(True) 23 | 24 | class W2(Guy): 25 | __doc__=""" 26 | <script> 27 | guy.init( async function() { 28 | if(await self.step2()) 29 | document.location.href="/W1" 30 | }) 31 | </script> 32 | 2 33 | """ 34 | def __init__(self,param): 35 | Guy.__init__(self) 36 | assert param == "42" 37 | def step2(self): 38 | return True 39 | 40 | t=W1() 41 | ok=runner(t) 42 | assert ok 43 | 44 | def test_redirect_exit(runner): # same concept as test_600_redirect.py ... but with better url 45 | 46 | class W1(Guy): 47 | __doc__=""" 48 | <script> 49 | guy.init( async function() { 50 | document.location.href="/W2?param=42" // the param is ignored 51 | }) 52 | </script> 53 | 1 54 | """ 55 | 56 | class W2(Guy): 57 | __doc__=""" 58 | <script> 59 | guy.init( async function() { 60 | self.end() 61 | }) 62 | </script> 63 | 2 64 | """ 65 | def end(self): 66 | assert self.parent 67 | self.exit(True) 68 | 69 | 70 | t=W1() 71 | 72 | ok=runner(t) # it's W2 which exit 73 | assert ok 74 | 75 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. 3 | // Pointez pour afficher la description des attributs existants. 4 | // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File (Integrated Terminal)", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal" 13 | }, 14 | { 15 | "name": "Python: Attach", 16 | "type": "python", 17 | "request": "attach", 18 | "port": 5678, 19 | "host": "localhost" 20 | }, 21 | { 22 | "name": "Python: Django", 23 | "type": "python", 24 | "request": "launch", 25 | "program": "${workspaceFolder}/manage.py", 26 | "console": "integratedTerminal", 27 | "args": [ 28 | "runserver", 29 | "--noreload", 30 | "--nothreading" 31 | ], 32 | "django": true 33 | }, 34 | { 35 | "name": "Python: Flask", 36 | "type": "python", 37 | "request": "launch", 38 | "module": "flask", 39 | "env": { 40 | "FLASK_APP": "app.py" 41 | }, 42 | "args": [ 43 | "run", 44 | "--no-debugger", 45 | "--no-reload" 46 | ], 47 | "jinja": true 48 | }, 49 | { 50 | "name": "Python: Current File (External Terminal)", 51 | "type": "python", 52 | "request": "launch", 53 | "program": "${file}", 54 | "console": "externalTerminal" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /docs/howto_build_exe_windows.md: -------------------------------------------------------------------------------- 1 | # How to build an exe for Windows 2 | 3 | It can be very useful to distribute and exe on Microsoft Windows platforms. By the way, you can freeze an executable on all platforms. But the following lines are for Windows platform. 4 | 5 | You will need [pyinstaller](https://www.pyinstaller.org/) ! 6 | 7 | ## A "light" one, with the need of chrome on the host 8 | 9 | (Who doesn't have chrome on its computer ?!) 10 | 11 | If yours users have chrome installed. It's the best option : the exe will reuse the installed chrome in "app mode". The exe will be lighter (6mo min) 12 | 13 | It's the best option for `app.run()` or `app.serve()` modes in your main py file. 14 | 15 | pyinstaller.exe YourGuyApp.py --noupx --onefile --noconsole --exclude-module cefpython3 --add-data="static;static" 16 | 17 | Notes: 18 | 19 | - `noupx` : because, with upx it gives me errors ;-) 20 | - `onefile` : to embed all needed runtime files. 21 | - `noconsole` : like you want ... 22 | - `exclude-module cefpython3` : So you will need to have chrome on the host machine to be able to run the exe. 23 | - `add-data="static;static"` : to embed yours static file for rendering (css, images ...) 24 | 25 | ## A full one ; all included 26 | 27 | If you target an unknow windows computer, perhaps you should embed a chrome in the exe. It's possible with [cefpython3](https://pypi.org/project/cefpython3/) module. 28 | 29 | Install cefpython 30 | 31 | python3 -m pip install cefpython3 32 | 33 | And change your `app.run()` into `app.runCef()` in your main py file. 34 | 35 | pyinstaller.exe YourGuyApp.py --noupx --onefile --noconsole --add-data="static;static" 36 | 37 | Notes: 38 | 39 | - your exe will be bigger (60mo min) 40 | - `noupx` : because, with upx it gives me errors ;-) 41 | - `onefile` : to embed all needed runtime files. 42 | - `noconsole` : like you want ... 43 | - `add-data="static;static"` : to embed yours static file for rendering (css, images ...) 44 | -------------------------------------------------------------------------------- /docs/multiple.md: -------------------------------------------------------------------------------- 1 | # Multiple instances 2 | 3 | Not like the good old [wuy](https://github.com/manatlan/wuy). With **guy** : you can use multiple guy's instance ! It's the main new feature over **wuy**. 4 | 5 | You can declare many Guy's class, and use them in a same app : it's easier to make bigger app ; you can leverage your logic/ui in multiple guy's class component. 6 | 7 | You can use theses others guy's class, using a simple **Navigate to another window** 8 | 9 | !!! info 10 | Note that the 'main instance' refers to the one which starts the loop. This instance will live til its dead (exit). Others guy's instances are (re)created on demand. 11 | So, the main instance can be useful to store persistent data during the life of the guy's app. Each instances have always access to the main one, using [self.parent](server.md#selfparent). 12 | 13 | 14 | ## Navigate to another window 15 | 16 | Consider this guy's app: 17 | 18 | ```python 19 | from guy import Guy 20 | 21 | class Page2(Guy): 22 | """ 23 | <<txt>> 24 | <a href="/Page1">go to Page1</a> 25 | <a href="/">go to Page1 too !</a> 26 | """ 27 | def __init__(self,txt): 28 | Guy.__init__(self) 29 | self.txt = txt 30 | 31 | class Page1(Guy): 32 | """ 33 | <a href="/Page2?txt=Hello">go to Page2</a> 34 | """ 35 | 36 | if __name__ == "__main__": 37 | app=Page1() 38 | app.run() 39 | ``` 40 | 41 | When the app is started, the `Page1` will be rendered, and you can navigate to `Page2`, and go back to `Page1`. Each page/class is available under its name. 42 | 43 | The main instance, is the default one, and is available at root ('/') too. 44 | 45 | BTW, when you are on `Page2`, `Page1` methods are not available anymore, and vis versa (the other instance is "dead", in fact). 46 | 47 | See [testRedirect.py](https://github.com/manatlan/guy/blob/master/testRedirect.py) 48 | 49 | !!! info 50 | - All windows share the same socket ! But each instance (on server side) is unique to a client. 51 | - Since >= 0.5.0, query parameters are used to resolve the constructor signature 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | MYCACHE/ 3 | __pycache__/ 4 | __pycache__/* 5 | *.py[cod] 6 | *.pyc 7 | *$py.class 8 | reqman.html 9 | pip-wheel-metadata/ 10 | SciTE.properties 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | config.json 107 | 108 | android/.buildozer 109 | android/bin/ 110 | tests/__pycache__/ 111 | .pytest_cache/ 112 | .pytest_cache/* 113 | .pytest_cache/*/* 114 | .pytest_cache/*/*/* 115 | .pytest_cache/*/*/*/* 116 | webrtc_event_logs/ 117 | .pytest_cache/v/cache/nodeids 118 | -------------------------------------------------------------------------------- /docs/howto_build_whl_package.md: -------------------------------------------------------------------------------- 1 | # How to build a WHL package for pypi 2 | 3 | You can create a pypi-package to distribute your app/tool ! 4 | It's really easy with [poetry](https://python-poetry.org/) 5 | 6 | First of all : create your package, from a console : 7 | 8 | poetry new myapp 9 | 10 | Edit your newly `myapp/pyproject.toml` to match : 11 | 12 | ```toml hl_lines="7 8 12" 13 | [tool.poetry] 14 | name = "myapp" 15 | version = "0.1.0" 16 | description = "" 17 | authors = ["you <you@gmail.com>"] 18 | 19 | [tool.poetry.scripts] # <-- create a 'myapp' command 20 | myapp = 'myapp:main' 21 | 22 | [tool.poetry.dependencies] 23 | python = "^3.7" 24 | guy = "^0.4" # <-- add a dependency to guy 25 | 26 | [tool.poetry.dev-dependencies] 27 | pytest = "^3.0" 28 | 29 | [build-system] 30 | requires = ["poetry>=0.12"] 31 | build-backend = "poetry.masonry.api" 32 | ``` 33 | 34 | I've setuped a console script in section `[tool.poetry.scripts]`. And I've added a dependency to `guy` in `[tool.poetry.dependencies]`. 35 | 36 | Now, you just need to add the entry point `main()` (and the core app ;-) in your package ... 37 | 38 | Edit the file `myapp/myapp/__init__.py` like that : 39 | 40 | ```python 41 | __version__ = '0.1.0' 42 | 43 | from guy import Guy 44 | 45 | class App(Guy): 46 | """ hello """ 47 | 48 | def main(): # <-- the entry point for the script !! 49 | App().run() 50 | 51 | if __name__=="__main__": 52 | main() 53 | ``` 54 | 55 | !!! info 56 | If you plan to declare your html in a static html file : just put your static datas in the `myapp/myapp/static` folder, 57 | and they will be be embbeded in the package (as `package_data`). Guy (>=0.4.0) will be able to resolve them in 58 | the installed package. 59 | 60 | 61 | And you are ! Just build your package (place you in the folder where `pyproject.toml` sits) 62 | 63 | poetry build 64 | 65 | And it's done ! 66 | 67 | You can distribute your package (`dist/myapp-0.1.0-py3-none-any.whl`), publish it to pypi.org ... 68 | 69 | ... or install it : 70 | 71 | python3.7 -m pip install --user --force dist/myapp-0.1.0-py3-none-any.whl 72 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Guy 2 | 3 | **Guy** is a python3 module to make a simple GUI for your python3 script, using html/js/css technologies (a little bit like [electron](https://electronjs.org/)). 4 | It borrows the idea from [python-eel](https://nitratine.net/blog/post/python-gui-using-chrome/), but provide a lot more things. 5 | 6 | The main idea, is to reuse the installed chrome app on the host. So your script (or your freezed app) stays at the minimal footprint. But your user needs to have Chrome (or chromium) on its computer, to run your script/app. 7 | 8 | If you want to release a standalone/freezed app, with all included (your script + a chrome container). You can use a special mode with [cefpython3](https://github.com/cztomczak/cefpython). But the footprint will be around 60mo (like an electron app). But you can ;-) 9 | 10 | There are 3 modes to release your app : 11 | 12 | * **app**: your user will need to have a chrome instance. The GUI will be handled by a chrome instance, runned in "app mode". (on android/ios, the GUI will be handled by webViewClient/kivy) 13 | * **cef**: (stands for cefpython3): All is embedded, it's the embedded cef instance which will handle your GUI. 14 | * **server**: it will act as an http server, and any browsers can handle your GUI. So, there can be multiple clients ! 15 | 16 | Like you understand, your GUI should be built with HTML/JS/CSS. Under the hood, **guy** provides a simple mechanisms (with websockets) to interact with the python technologies. Your GUI can be native HTML/JS/CSS or any modern js frameworks : vuejs, angular, react, etc ... 17 | 18 | The **app mode** can be runned on an Android device, using kivy/buildozer toolchain (for building an apk). Understand that the same app can be runned on any android devices or on any computer (win, mac, *nix ...), without any modifications. 19 | 20 | 21 | !!! note "Main goal" 22 | With **guy**, you can provide quickly a frontend for your tool. 23 | When freezed, the minimal size is nearly 6mo (under windows10). 24 | Your users can use it, as is (if they've got chrome installed). 25 | 26 | 27 | [![Join the chat at https://gitter.im/guy-python/community](https://badges.gitter.im/jessedobbelaere/ckeditor-iconfont.svg)](https://gitter.im/guy-python/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **GUY** is a py3 module, which let you quickly release a GUI (html/js) for yours python (>=3.5) scripts, targetting **any** platforms ... and **android** too. 2 | 3 | A simple **guy's app** code, could be : 4 | 5 | ```python 6 | from guy import Guy 7 | 8 | class Simple(Guy): 9 | """<button onclick="self.test()">test</button>""" 10 | 11 | async def test(self): 12 | print("Your name is", await self.js.prompt("What's your name ?") ) 13 | 14 | if __name__ == "__main__": 15 | app=Simple() 16 | app.run() 17 | ``` 18 | 19 | A **guy's app** can be runned in 3 modes : 20 | 21 | - can reuse a chrome browser (in app mode), on the host. To keep the minimal footprint. (**app mode**) 22 | - can embbed its CEF (like electron) (thanks cefpython3), to provide all, to the users. (**cef mode**) 23 | - can act as a classical web server. Any browser can be a client (**server mode**) 24 | 25 | A **guy's app** can be released as : 26 | 27 | - a simple py3 file, with only guy dependancy (**app mode** & **server mode**)), or with guy+cefpython3 dependancies (**cef mode**)) 28 | - a freezed executable (pyinstaller compliant) (all modes) 29 | - a [pip/package app](https://guy-docs.glitch.me/howto_build_whl_package/) (all modes) 30 | - an **apk** for android (with buildozer/kivy) (**app mode** only) 31 | 32 | Read the [Guy's DOCUMENTATION](https://manatlan.github.io/guy/) ! 33 | 34 | Available on : 35 | 36 | - [Guy's Github](https://github.com/manatlan/guy) 37 | - [Guy's Pypi](https://pypi.org/project/guy/) 38 | 39 | Here is a [demo](https://starter-guy.glitch.me/#/) ([sources](https://glitch.com/edit/#!/starter-guy)), of a simple guy's app (server mode). 40 | 41 | Here is a [demo](https://starter-guy-vuejs.glitch.me/#/) ([sources](https://glitch.com/edit/#!/starter-guy-vuejs)), of a guy's app serving a vuejs/sfc UI. 42 | 43 | Here is a simple **guy's app** (**app mode**): 44 | <p align="center"> 45 | <table> 46 | <tr> 47 | <td valign="top"> 48 | On Ubuntu<br> 49 | <img src="https://manatlan.github.io/guy/shot_ubuntu.png" width="300" border="1" style="border:1px solid black"/> </td> 50 | <td valign="top"> 51 | On Android10<br> 52 | <img src="https://manatlan.github.io/guy/shot_android10.jpg" width="300" border="1" style="border:1px solid black"/> 53 | </td> 54 | </tr> 55 | </table> 56 | </p> 57 | 58 | [![Join the chat at https://gitter.im/guy-python/community](https://badges.gitter.im/jessedobbelaere/ckeditor-iconfont.svg)](https://gitter.im/guy-python/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 59 | 60 | If you want to build guy app, without any html/js/css knowlegments, you can try [gtag](https://github.com/manatlan/gtag) : it's a guy sub module which let you build GUI/GUY app in [more classical/python3 way](https://github.com/manatlan/gtag/wiki). 61 | -------------------------------------------------------------------------------- /testPyCallJs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | # -*- coding: utf-8 -*- 3 | import guy 4 | 5 | 6 | class TestPyCallJs(guy.Guy): 7 | """ 8 | <script> 9 | 10 | function myjsmethod(a,b) { 11 | document.body.innerHTML+= `sync call (${a},${b})<br>`; 12 | return Math.random(); 13 | } 14 | 15 | async function myLONGjsmethodAsync(a,b) { 16 | document.body.innerHTML+= `async call long (${a},${b})...`; 17 | await new Promise(r => setTimeout(r, 2000)); 18 | document.body.innerHTML+= `...ok<br>`; 19 | return Math.random(); 20 | } 21 | 22 | async function myjsmethodAsync(a,b) { 23 | document.body.innerHTML+= `async call (${a},${b})<br>`; 24 | return Math.random(); 25 | } 26 | 27 | function myKAPUTTjsmethod() { 28 | callInError(); // raise an exception on js side 29 | } 30 | 31 | async function myKAPUTTjsmethodAsync() { 32 | callInError(); // raise an exception on js side 33 | } 34 | 35 | </script> 36 | <button onclick="self.test_ok()">call js ok</button> 37 | <button onclick="self.test_ok_async()">call async js ok</button> 38 | <button onclick="self.test_long_async()">call async long js ok</button> 39 | <button onclick="self.test_NF()">call js not found</button> 40 | <button onclick="self.test_ko()">call js ko</button> 41 | <button onclick="self.test_ko_async()">call async js ko</button> 42 | <button onclick="self.test_prompt()">test promt()</button> 43 | <br/> 44 | """ 45 | size=(500, 300) # set the size of the client window 46 | 47 | async def test_prompt(self): 48 | name = await self.js.prompt("What's your name ?") 49 | print("==========js returns=========>",name) 50 | return "ok prompt" 51 | 52 | async def test_ok(self): 53 | r=await self.js.myjsmethod("Python World!",42) 54 | print("==========js returns=========>",r) 55 | return "ok sync" 56 | 57 | async def test_ok_async(self): 58 | r=await self.js.myjsmethodAsync("Python World!",44) 59 | print("==========js returns=========>",r) 60 | return "ok async" 61 | 62 | async def test_long_async(self): 63 | r=await self.js.myLONGjsmethodAsync("Python World!",45) 64 | print("==========js returns=========>",r) 65 | return "ok async" 66 | 67 | async def test_NF(self): 68 | r=await self.js.myUNDECLAREDjsmethod() 69 | print("==========js returns=========>",r) 70 | return "nf" 71 | 72 | async def test_ko(self): 73 | r=await self.js.myKAPUTTjsmethod() 74 | print("==========js returns=========>",r) 75 | return "ko" 76 | 77 | async def test_ko_async(self): 78 | r=await self.js.myKAPUTTjsmethodAsync() 79 | print("==========js returns=========>",r) 80 | return "ko" 81 | 82 | if __name__ == "__main__": 83 | TestPyCallJs().run(log=True) -------------------------------------------------------------------------------- /docs/client.md: -------------------------------------------------------------------------------- 1 | # Client side : guy.js 2 | 3 | Not like the good old [wuy](https://github.com/manatlan/wuy); javascript's apis are in two objetcs : 4 | 5 | - **guy** : to handle the core of guy. 6 | - **self** : to handle the declared methods in the guy's class, on [server side](server.md) 7 | 8 | ## Guy's apis 9 | 10 | ---- 11 | ###`guy.init( function() { ... } )` 12 | 13 | Will run the `function` when everything is started. It's a good place to start your logic. 14 | 15 | BTW, since >=0.4.3 ... it's a better practice to call a js method, from py side, to start your logic ... like this : 16 | 17 | ```python 18 | class Example(Guy): 19 | """ 20 | <script> 21 | function start() { 22 | // .... 23 | } 24 | </script> 25 | """ 26 | async def init(self): 27 | await self.js.start() 28 | ``` 29 | 30 | ---- 31 | ###`guy.on( event, function(arg1, arg2, ...) { ... } )` 32 | 33 | To listen to an `event` ... 34 | 35 | It returns a method to unsubscribe the listenner. 36 | 37 | ---- 38 | ###`guy.emit( event, arg1, arg2, ...)` 39 | 40 | To emit an `event` to all connected clients. 41 | 42 | It's a non-sense in `app` or `cef` mode : because there is only one client. It only got sense in `server` mode. 43 | 44 | ---- 45 | ###`guy.emitMe( event, arg1, arg2, ...)` 46 | 47 | To emit an `event` to the current client (only me;-)). 48 | 49 | ---- 50 | ###`guy.fetch( url, options )` 51 | 52 | Same as [`window.fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), but it's the server which will do the request, to avoid CORS issues. 53 | 54 | ---- 55 | ###`guy.exit( returnValue=None )` 56 | Exit the app. 57 | 58 | If you want to get a "returnValue", you can set here the returned value, in py side: 59 | 60 | ```python 61 | myapp=MyGuyApp() 62 | returnValue = myapp.run() 63 | ``` 64 | 65 | ---- 66 | ###`async guy.cfg` 67 | A place to get/set vars, which will be stored on server side, in a `config.json` file, where the main executable is runned. 68 | (if the guy'app is embedded in a pip/package, the config file will be stored in ``~/.<package_name>.json`) 69 | 70 | To set a var 'name', in js side : 71 | 72 | ```javascript 73 | guy.cfg.name = "Elton"; 74 | ``` 75 | 76 | To get a var 'name', in js side : 77 | 78 | ```javascript 79 | var name = await guy.cfg.name; 80 | ``` 81 | 82 | 83 | ## Self's apis 84 | 85 | It's all the apis which have been defined in the class instance. 86 | 87 | If you have a class like that, on py side: 88 | ```python 89 | class Simple(Guy): 90 | """<button onclick="self.test()">test</button>""" 91 | 92 | def test(self): 93 | print("hello world") 94 | 95 | ``` 96 | 97 | You wil have a `self.test()` method in client side ! 98 | 99 | Theses methods can be sync or async, depending on your need. 100 | 101 | ###`self.exit( returnValue=None )` 102 | Exit the current instance, if it's the main instance : it quits the app. 103 | 104 | If you want to get a "returnValue", you can set the returned value, which will be returned in py side: 105 | 106 | ```python 107 | myapp=MyGuyApp() 108 | returnValue = myapp.run() 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/run.md: -------------------------------------------------------------------------------- 1 | # Run your app 2 | 3 | Admit you've got an app: 4 | 5 | ```python 6 | from guy import Guy 7 | 8 | class YourApp(Guy): 9 | ... 10 | 11 | if __name__ == "__main__": 12 | app=YourApp() 13 | app.run() #<- this is how to run it ;-) 14 | ``` 15 | 16 | !!! info 17 | Since 0.5.1 versions, you can use `autoreload`'s mode to help you during dev process (in production : don't set the `autoreload` to `True`) 18 | 19 | 20 | ## The differents modes 21 | Each method starts the loop (and provide the GUI). And when exiting : it returns the exit's returnValue (see js/exit() or py/exit()) 22 | ### app mode 23 | 24 | Use `app.run()` 25 | 26 | Classical mode, on desktop : it uses the installed chrome browser in app mode. (it's the way to run on android too) 27 | 28 | Optionnal parameters: 29 | 30 | - one: (bool), permit to run just once instance at the same time (if True, running a second one will re-focus to the already runned one), default: False 31 | - log: (bool) enable logging (client & server side) (don't have any effect on android), default: False 32 | - autoreload: (bool) autoreload on changes (don't have any effect on android), default: False 33 | - args: (list) add any additional startup arguments for the browser. _Example:_ `args=["--autoplay-policy=no-user-gesture-required"]` 34 | 35 | 36 | `app.run(one=True, args=["--autoplay-policy=no-user-gesture-required"])` 37 | 38 | To be able to store things in js/localStorage, you must use the `one` parameter, to make storage persistent. By default, storage is not persistent, and removed after each use! 39 | 40 | 41 | ### cef mode 42 | 43 | Use `app.runCef()` 44 | 45 | Special mode for desktop : when you want to provide a standalone app, with all included. You will need cefpython3 ! 46 | (and you user don't need to have a chrome/chromum installed) 47 | 48 | Optionnal parameters: 49 | 50 | - one: (bool), permit to run just once instance at the same time (if True, running a second one will re-focus to the already runned one), default: False 51 | - log: (bool) enable logging (client & server side), default: False 52 | - autoreload: (bool) autoreload on changes, default: False 53 | 54 | To be able to store things in js/localStorage, you must use the `one` parameter, to make storage persistent. By default, storage is not persistent, and removed after each use! 55 | 56 | 57 | ### server mode 58 | 59 | Use `app.serve()` 60 | 61 | Server mode, for servers. 62 | 63 | Optionnal parameters: 64 | 65 | - log: (bool) enable logging (client & server side), default: False 66 | - port: (number) listening port, default: 8000. 67 | - open: (bool) open default browser to the client, default: True 68 | - autoreload: (bool) autoreload on changes, default: False 69 | 70 | 71 | ## To summarize the choice 72 | 73 | Just a table to help you to select the best mode for your needs 74 | 75 | | Mode : | App | Cef | Server | 76 | |:---------------------------------------|:---:|:----:|:------:| 77 | | Your users need chrome to run your app | yes | no | no | 78 | | Works on android/apk | yes | no | no | 79 | | Works on any OS | yes | yes | yes | 80 | | Your script is freezable, on any OS | yes | yes | yes | 81 | | Your script is pip packageable | yes | yes | yes | 82 | | Minimum size of the freezed executable | 6mo | 60mo | 6mo | 83 | | Many clients at same time | no | no | yes | 84 | | Host your app on a server (glitch.com) | no | no | yes | 85 | 86 | !!! info 87 | Pip-packageable is only enabled for guy >= 0.4.0 88 | -------------------------------------------------------------------------------- /android/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | from guy import Guy,FULLSCREEN 4 | import asyncio,datetime 5 | 6 | 7 | ## buildozer android debug deploy run 8 | 9 | CSS=""" 10 | 11 | main {filter: blur(20px);} 12 | 13 | div#back {position:fixed;z-index:1;top:0px;left:0px;right:0px;bottom:0px; 14 | background-color: rgba(0,0,0, .2); 15 | text-align:center; 16 | text-shadow: 0 0 0.5em black, 0 0 0.2em white; 17 | box-shadow: inset 0 0 0.5em black, 0 0 0.2em white; 18 | color: white; 19 | } 20 | div#back button,div#back input {box-shadow: 0 0 0.5em black, 0 0 0.2em white;margin:4px} 21 | """ 22 | 23 | 24 | class Spinner(Guy): 25 | size=(300,20) 26 | __doc__=""" 27 | <style> 28 | """+CSS+""" 29 | </style> 30 | <div id='back'> 31 | <h3>Wait...</h3> 32 | </div> 33 | """ 34 | 35 | 36 | class MsgBox(Guy): 37 | size=(300,150) 38 | __doc__=""" 39 | <style> 40 | """+CSS+""" 41 | </style> 42 | <div id='back'> 43 | <h3><<title>></h3> 44 | <button onclick="self.exit()">OK</button> 45 | </div> 46 | """ 47 | def __init__(self,title="unknown"): 48 | Guy.__init__(self) 49 | self.title=title 50 | 51 | 52 | 53 | class Confirm(Guy): 54 | size=(300,20) 55 | __doc__=""" 56 | <style> 57 | """+CSS+""" 58 | </style> 59 | <div id='back'> 60 | <h3><<title>></h3> 61 | <button onclick="self.confirmChoice(true)">OK</button> 62 | <button onclick="self.confirmChoice(false)">Cancel</button> 63 | </div> 64 | """ 65 | def __init__(self,title): 66 | Guy.__init__(self) 67 | self.title=title 68 | self.ret=False 69 | 70 | def confirmChoice(self,val): 71 | self.ret=val 72 | self.exit() 73 | 74 | 75 | class Prompt(Guy): 76 | size=(300,20) 77 | __doc__=""" 78 | <style> 79 | """+CSS+""" 80 | div#back form {display:inline} 81 | </style> 82 | <div id='back'> 83 | <h3><<title>></h3> 84 | <form onsubmit="self.post( this.txt.value ); return false"> 85 | <input id='n' name="txt" value="<<txt>>" onfocus="var val=this.value; this.value=''; this.value= val;" /> 86 | <button> > </button> 87 | </form> 88 | <button onclick="self.exit()">Cancel</button> 89 | </div> 90 | <script> 91 | document.querySelector("#n").focus() 92 | </script> 93 | """ 94 | def __init__(self,title,txt=""): 95 | Guy.__init__(self) 96 | self.title=title 97 | self.txt=txt 98 | self.ret=None 99 | 100 | def post(self,txt): 101 | self.ret=txt 102 | self.exit() 103 | 104 | 105 | class Win(Guy): 106 | size=(400,500) 107 | #~ size = FULLSCREEN 108 | __doc__=""" 109 | <style> 110 | body {background:#EEE; 111 | -webkit-touch-callout: none; 112 | -webkit-user-select: none; 113 | -khtml-user-select: none; 114 | -moz-user-select: none; 115 | -ms-user-select: none; 116 | user-select: none; 117 | font-size: 1.5em; 118 | } 119 | * {font-size: 1em;} 120 | * {font-family: arial;-webkit-tap-highlight-color: transparent;outline: none;} 121 | .click, button {cursor:pointer;color:blue} 122 | button {border-radius:4px;background:blue;color:white } 123 | 124 | 125 | </style> 126 | <script> 127 | 128 | async function change(title,item,el) { 129 | var w=await self.winPrompt(title, el.innerText ) 130 | var r=await w.run() 131 | if(r.ret) { 132 | el.innerText=r.ret 133 | guy.cfg[item]=r.ret 134 | } 135 | } 136 | 137 | async function exit() { 138 | var w=await self.winConfirm() 139 | var r = await w.run() 140 | if(r.ret) guy.exit() 141 | } 142 | 143 | async function mbox() { 144 | var w=await self.winMbox() 145 | var r=await w.run(); 146 | } 147 | 148 | async function wait() { 149 | var w=await self.winSpinner() 150 | setTimeout( w.exit, 1000) 151 | } 152 | 153 | document.addEventListener("contextmenu", function (e) { 154 | e.preventDefault(); 155 | }, false); 156 | </script> 157 | <main> 158 | <button style="float:right" onclick="exit()">X</button> 159 | <h1>My GuyAPP ;-)</h1> 160 | <hr/> 161 | <div> 162 | Name: <span class='click' onclick="change('Name ?','name', this)"><<defaultName>></span> 163 | </div> 164 | <div> 165 | Surname: <span class='click' onclick="change('Surname ?','surname', this)"><<defaultSurname>></span> 166 | </div> 167 | 168 | <button onclick="mbox()">test</button> 169 | <button onclick="wait()">Wait</button> 170 | </main> 171 | """ 172 | def __init__(self): 173 | Guy.__init__(self) 174 | self.defaultName=self.cfg.name or "empty" 175 | self.defaultSurname=self.cfg.surname or "empty" 176 | 177 | 178 | def winPrompt(self,txt,val): 179 | return Prompt(txt,val) 180 | 181 | def winConfirm(self): 182 | return Confirm("Quit ?") 183 | 184 | def winMbox(self): 185 | return MsgBox("Just a message box") 186 | 187 | def winSpinner(self): 188 | return Spinner() 189 | 190 | if __name__ == "__main__": 191 | x=Win() 192 | x.run() 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /testAll.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | import guy,asyncio 3 | 4 | """ 5 | here, i will try to test a max of features in one test/file ... 6 | (currently, it's the beginning) 7 | 8 | When concluant, will be integrated in pytests ;-) 9 | """ 10 | 11 | @guy.http(r"/item/(\d+)") 12 | def getItem(web,number): 13 | web.write( "item %s"%number ) 14 | 15 | class MyMethods: 16 | def testInheritedMethod(self): 17 | return "ok" 18 | 19 | class App(guy.Guy,MyMethods): 20 | """ 21 | <script> 22 | 23 | //==================================================== 24 | var MARKS=[] 25 | function mark(t) { 26 | MARKS.push(t) 27 | document.body.innerHTML+= `<li>${t}</li>`; 28 | } 29 | 30 | guy.on("evtMark", mark) 31 | 32 | async function finnish() { 33 | self.exit( MARKS ) 34 | } 35 | //==================================================== 36 | 37 | async function callOk(v) { 38 | return await self.mulBy2(v) 39 | } 40 | 41 | async function callKo() { 42 | await self.unknowMethod() 43 | return "ok" 44 | } 45 | 46 | 47 | async function changeConfig() { 48 | let v = await guy.cfg.cptClient || 0; 49 | guy.cfg.cptClient = v+1; 50 | mark("js: guy.cfg set/get : ok") 51 | } 52 | 53 | async function testFetch() { 54 | let q=await window.fetch("/item/42") 55 | let x=await q.text() 56 | if(x=="item 42") mark("windows fetch/hook : ok") 57 | } 58 | 59 | async function testGFetch() { 60 | let q=await guy.fetch("/item/42") 61 | let x=await q.text() 62 | if(x=="item 42") mark("guy fetch/hook : ok") 63 | } 64 | 65 | function testSubVar() { 66 | return "<<myVar>>"; 67 | } 68 | 69 | 70 | async function callTestJsReturn() { 71 | await self.testJsReturn() 72 | } 73 | 74 | async function callTestInheritedMethod() { 75 | let t=await self.testInheritedMethod() 76 | mark("Call an inherited method : "+t) 77 | } 78 | 79 | 80 | </script> 81 | <button onclick="self.init()">replay</button> 82 | """ 83 | myVar="ThisIsAVar" 84 | 85 | 86 | async def init(self): 87 | # call a simple js method at start 88 | await self.js.mark("py.init autocalled : ok") 89 | 90 | # test that's the var substitution mechanism is working 91 | v=await self.js.testSubVar() 92 | await self.js.mark("var substituion : %s" % (v==App.myVar and "ok" or "ko")) 93 | 94 | # test the call of a real js window.method 95 | v=await self.js.parseInt("42") 96 | await self.js.mark("call a real js method : %s" % (v==42 and "ok" or "ko")) 97 | 98 | # call a js method which call a py method with param 99 | x=await self.js.callOk(42) 100 | await self.js.mark("callOk : %s " % ("ok" if x==84 else "ko")) 101 | 102 | # call a js method which call a unknonw py method 103 | try: 104 | x=await self.js.callKo() 105 | await self.js.mark("callKo : ko") 106 | except guy.JSException: 107 | await self.js.mark("callKo : ok") 108 | 109 | # call a unknown js method 110 | try: 111 | x=await self.js.unknown() 112 | await self.js.mark("call unknown js : ko") 113 | except guy.JSException: 114 | await self.js.mark("call unknown js : ok") 115 | 116 | # send a event to me 117 | await self.emitMe("evtMark","Try a perso event: ok") 118 | 119 | # send a event to all 120 | await self.emit("evtMark","Try a event to all: ok") 121 | 122 | # change config client side 123 | await self.js.changeConfig() 124 | 125 | # change config server side 126 | v=self.cfg.cptServer or 0 127 | self.cfg.cptServer = v+1 128 | await self.emit("evtMark","py: self.cfg set/get : ok") 129 | 130 | # test the http hook with js/window.fetch & js/guy.fetch (the proxy) 131 | w1=self.js.testFetch() 132 | w2=self.js.testGFetch() 133 | await asyncio.gather(w1,w2) 134 | 135 | # test testJsReturn 136 | await self.js.callTestJsReturn() 137 | 138 | # test TestInheritedMethod 139 | await self.js.callTestInheritedMethod() 140 | 141 | await self.js.finnish() 142 | 143 | def mulBy2(self,v): 144 | return v*2 145 | 146 | async def testJsReturn(self): 147 | return dict( script="mark('returning dict/script : ok')" ) #it's evil! 148 | 149 | 150 | 151 | if __name__ == "__main__": 152 | app=App() 153 | ll=app.run() 154 | print(">>>",ll) 155 | assert ll==['py.init autocalled : ok', 'var substituion : ok', 'call a real js method : ok', 'callOk : ok ', 'callKo : ok', 'call unknown js : ok', 'Try a perso event: ok', 'Try a event to all: ok', 'js: guy.cfg set/get : ok', 'py: self.cfg set/get : ok', 'windows fetch/hook : ok', 'guy fetch/hook : ok', 'returning dict/script : ok', 'Call an inherited method : ok'] 156 | 157 | -------------------------------------------------------------------------------- /testSudoku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | from guy import Guy 3 | import random 4 | 5 | ##################################################### my simplest sudoku resolver ;-) 6 | free = lambda n: set("123456789.") ^ set(n) 7 | carre = lambda g,x,y: g[y*9+x:y*9+x+3] + g[y*9+x+9:y*9+x+12] + g[y*9+x+18:y*9+x+21] 8 | inter = lambda g,x,y: free(g[x::9]) & free(g[y*9:y*9+9]) & free(carre(g,(x//3)*3,(y//3)*3)) 9 | tri = lambda k: len(k[0]) 10 | 11 | def resolv(x): 12 | idxs = sorted([(inter(x,i%9,i//9),i) for i,c in enumerate(x) if c=='.'],key=tri) 13 | if not idxs: return x 14 | for c in idxs[0][0]: 15 | ng=resolv(x[:idxs[0][1]] + c + x[idxs[0][1]+1:]) 16 | if ng: return ng 17 | ##################################################### 18 | 19 | class Sudoku(Guy): 20 | """ 21 | <style> 22 | body {margin:0px;text-align:center;background:buttonface} 23 | 24 | div#grid { 25 | margin:8px; 26 | border:2px solid black; 27 | display:inline-block; 28 | } 29 | body.bad input { 30 | color:red; 31 | } 32 | body.bad button#r { 33 | pointer-events:none; 34 | color:#FFF; 35 | } 36 | 37 | div#grid > input:read-only { 38 | color:#AAA; 39 | } 40 | div#grid > input { 41 | text-align: center; 42 | font-size: 30px; 43 | width:40px; 44 | height:40px; 45 | display:block; 46 | border: 1px solid #ccc; 47 | float: left; 48 | } 49 | 50 | div#grid > input:nth-child(9n+4), div#grid > input:nth-child(9n+7) { 51 | border-left:2px solid black; 52 | } 53 | 54 | div#grid > input:nth-child(19),div#grid > input:nth-child(20),div#grid > input:nth-child(21),div#grid > input:nth-child(22),div#grid > input:nth-child(23),div#grid > input:nth-child(24),div#grid > input:nth-child(25),div#grid > input:nth-child(26),div#grid > input:nth-child(27),div#grid > input:nth-child(46),div#grid > input:nth-child(47),div#grid > input:nth-child(48),div#grid > input:nth-child(49),div#grid > input:nth-child(50),div#grid > input:nth-child(51),div#grid > input:nth-child(52),div#grid > input:nth-child(53),div#grid > input:nth-child(54) { 55 | border-bottom:2px solid black; 56 | } 57 | 58 | div#grid > input:nth-child(9n+1) { 59 | clear:both; 60 | } 61 | 62 | </style> 63 | <div id="grid"></div> 64 | <br/> 65 | 66 | <button onclick="doClear()">Clear</button> 67 | <button onclick="doRandom()">Random</button> 68 | <button id="r" onclick="doResolv()">Resolv</button> 69 | 70 | <script> 71 | function setGrid(g) { 72 | document.body.className=""; 73 | let d = document.querySelector("#grid") 74 | d.innerHTML="" 75 | for(var i=1;i<=9*9;i++) { 76 | let c=g[i-1]; 77 | let h=document.createElement("input") 78 | h.id=`c${i}` ; 79 | if(c==".") { 80 | h.onclick=function() {this.select()} 81 | h.onchange=function() {doValid()} 82 | } 83 | else { 84 | h.value=c 85 | h.readOnly= true 86 | } 87 | d.appendChild( h ) 88 | } 89 | undo = null; 90 | } 91 | 92 | function getGrid() { 93 | var g=""; 94 | for(var i=1;i<=9*9;i++) { 95 | let c=document.querySelector(`#c${i}`).value.trim() 96 | g+=(c && "123456789".indexOf(c)>=0?c[0]:"."); 97 | } 98 | return g 99 | } 100 | 101 | function doClear() { 102 | setGrid(".................................................................................") 103 | } 104 | 105 | async function doValid() { 106 | let err=await self.checkValid( getGrid() ) 107 | document.body.className=err?"bad":""; 108 | } 109 | 110 | async function doRandom() { 111 | setGrid( await self.random() ) 112 | } 113 | 114 | var undo=null; 115 | async function doResolv() { 116 | if(undo==null) { 117 | let current = getGrid() 118 | let r=await self.resolv( current ) 119 | if(r) { 120 | setGrid( r ) 121 | undo = current; 122 | } 123 | } 124 | else 125 | setGrid(undo); 126 | } 127 | 128 | </script> 129 | """ 130 | size=(420,450) 131 | 132 | async def init(self): 133 | await self.js.setGrid( self.random() ) 134 | 135 | def resolv(self,g): 136 | gr=resolv(g) 137 | print("RESOLV: %s" % g) 138 | print("----->: %s" % gr) 139 | return gr 140 | 141 | def random(self): 142 | ll=80*["."] + [ str(random.randint(1,9)) ] 143 | random.shuffle(ll) 144 | 145 | ll=list(resolv("".join(ll))) 146 | for i in range(100): 147 | ll[ random.randint(0,80) ]="." 148 | return "".join(ll) 149 | 150 | def checkValid(self,g): 151 | check9=lambda g: all([g.count(c)==1 for c in g.replace(".","")]) 152 | for i in range(0,9): 153 | if not check9( g[i::9] ): return "Vertical trouble column %s"%i 154 | if not check9( g[i*9:i*9+9] ): return "Horiz trouble row %s"%i 155 | if not check9( carre(g,(i*3)%9,(i//3)*3) ): return "Trouble in %s square"%i 156 | 157 | if __name__=="__main__": 158 | app=Sudoku() 159 | app.run() 160 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 0.7.6 (30/10/2020) 2 | 3 | - EVOL: added _tornado to instance, cleanup event, afterServerStarted event (thks for the PR @robert-boulanger) 4 | 5 | 0.7.5 (28/06/2020) 6 | 7 | - FIX: the guy js is now included relatively (no more absolute path) 8 | 9 | 0.7.4 (27/05/2020) 10 | 11 | - FIX: autoreload was broken 12 | 13 | 0.7.3 (26/05/2020) 14 | 15 | - EVOL: Add guy.WSGUY to be able to set manually the ws server (ex: glitch app with custom domain)) (ex: wss://example.com) 16 | - EVOL: automatically apply a "wsguy" class on body, when socket-communication (client to server) 17 | 18 | 0.7.2 (15/05/2020) 19 | 20 | - It's the version which shoulded be the 0.7.1, and fix that bug for real ;-) ... sorry for the noise 21 | 22 | 0.7.1 (14/05/2020) 23 | 24 | - FIX: reload(f5) was broken, when using self.render() 25 | - remove INST replaced by Guy._instances 26 | - create the instance in Guy._instances at __init__ 27 | 28 | 29 | 0.7.0 (06/05/2020) : the REAL good one ;-) 30 | 31 | - EVOL (BROKE COMPATIBILITY): new way to return (.run()->x ,.runcef()->x,.serve()->x) with exit(x) : x is returned ! 32 | - FIX: (server mode : isolation context execution was broken for 0.4.3 < version <=0.6.0 ) since commit "bf869e1cad5d630c1a2f38858b2da98ecaae60ce" 33 | Now, it's a lot better ! 34 | - EVOL: remove (previously deprecated) "embedded window" (instanciateWindow) 35 | - EVOL: nice quit when cef is broken (cefpython3+py3.8.2 on linux) 36 | - EVOL: response content is gzipped now (thanks @icarito) 37 | - pytests 100%ok 38 | 39 | 0.6 (24/04/20) 40 | 41 | - new logo: and rendered as default favicon.ico 42 | - one mode available in app & cef mode : let run one instance only with chrome cache (stored belongs the guy's app). Else the app can't count - on cache/chrome ! (removed at end) 43 | - app-mode: new ChromeApp, better interaction with chrome !! 44 | - app-mode: when one mode on -> focus on current running (win+*nix) 45 | - cef-mode: when one mode on -> focus on current running (win only!!!) (broken on *nix) 46 | - app-mode: resize browser at start (no more js based) 47 | - app-mode: chrome process outputs to null 48 | - fix: logging server side 49 | - the use of embed window (returning guy class) is now (really) deprecated (wants to simplify) 50 | - app-mode: disable google translate 51 | 52 | 0.5.7: (14/04/2020) 53 | 54 | fix: dead socket on on_message 55 | 56 | 0.5.6: (09/04/2020) 57 | 58 | fix: js log was always on. now: it depends if the log is activated or on server side too 59 | 60 | 0.5.5: (26/03/2020) 61 | 62 | - FIX: on win, when freezed, cant be considered as module 63 | 64 | 0.5.4: (21/03/2020) 65 | 66 | - FIX: ability to read html files with encoding utf8 or cp1252 67 | - EVOL: expose config file path (py side) : self.cfg._file 68 | - EVOL: tornado application is available as "app" attribut on guy instance (for specials customizations), thanks @dferens ! 69 | 70 | 0.5.3: (11/02/2020) 71 | - FIX: thanks @icarito https://github.com/manatlan/guy/pull/8/commits/9477b548b91ae81bfc327dac7ba1ec80804f4f8d 72 | 73 | 0.5.2: (09/02/2020) 74 | - FIX: guy crashed when autoreload with no static folder 75 | 76 | 0.5.1: (09/02/2020) 77 | - EVOL : autoreload available 78 | 79 | 0.5.0: (08/02/2020) 80 | - BIGGEST CHANGES: 81 | - "real instances" (no more clonage) .. a lot simpler 82 | - better system to manage instances (same fo embbeded or redirected) 83 | - pyside: each window now have a reference (.parent) to the main instance (the one which starts all) 84 | - a lot of little fixes 85 | - more pytest coverage (mainly main features) 86 | - _render() -> render() and replace "guy.js" in all cases 87 | - resolve query params when redirecting to another instance for match the constructor 88 | 89 | 0.4.3: (01/02/2020) 90 | - EVOL: Py side : can call js method directly ( `await self.js.jsmethod(...)` ) 91 | 92 | 0.4.2: (31/01/2020) 93 | - FIX : trouble to find config folder when symbolic link used 94 | - EVOL: "init" can now be async too 95 | - FIX: the right "init" is now called when a instance is created (on ws call) 96 | 97 | 0.4.1: (31/01/2020) 98 | 99 | - FIX: trouble with venv (ability to find static data) 100 | 101 | 0.4.0: (26/01/2020) 102 | 103 | - BIG CHANGES : 104 | - Guy doesn't change/enforce the CWD !!! 105 | - Ability to be embbeded in a pip/package (see the how-to with poetry) 106 | - When pip-packaged : save in `~/.<package_name>.json` 107 | - The use of guy's config is now displayed in log (when log on) 108 | - use logging (no more print()) 109 | 110 | 0.3.9: (16/11/2019) 111 | 112 | - FIX: tornado/py3.8 on windows 113 | 114 | 0.3.8: (14/11/2019) 115 | 116 | - FIX: better regex to replace guy.js script 117 | 118 | changelog 0.3.7: initial public release (14/11/2019) 119 | 120 | - "/guy.js" refer to the main instance now (like in the past) 121 | - global method emit(event,*args) (old wsBroadcast()) 122 | - chrome's folder doesn't contains the port now ! (so same apps share the same chrome's cfg folder) 123 | - guy.on("evt", ...) -> return an unsubscriber method (like wuy) (thanks PR from alemoreau) 124 | - remove "reactivity commented code" 125 | 126 | changelog 0.3.6 "i-wall": 127 | 128 | - BIG CHANGE in jshandler ( guy.js -(when rendered)-> "/klassname/guy.js") (no more referer needed!!) 129 | - BUG FIXED: nb crash when sockets change !!!!!!! 130 | - BUG FIXED: when cloning instance : init() takes 1 positional argument but 2 were given 131 | - http handler decorator (full verb support +async or sync), and ability to return Guy Instance (redirect url) !!! 132 | - auto remove broken socket 133 | - better children rendered (new methods) 134 | - serve(...open=True...) to open browser by default 135 | 136 | changelog 0.3.5: 137 | 138 | - js handler now use urlparse (better) 139 | 140 | changelog 0.3.4: 141 | 142 | - ws reconnect on lost 143 | - js for instanciateWindow is now attached in dom, no more only eval'uated 144 | 145 | changelog 0.3.3: 146 | 147 | - compat py35 148 | 149 | changelog 0.3.2: 150 | 151 | - jshandler: remove queryparams from referer 152 | - _render: include "guy.js?<name>" to avoid history.back trouble for class with html embedded 153 | 154 | changelog 0.3.1: 155 | 156 | - MULTI PAGE, via children (useful in server mode !!!) 157 | - GLOBAL STATIC FOLDER VAR 158 | - remove js (=>) incompatibility for ie11 159 | 160 | changelog 0.3: 161 | 162 | - reactive property client side 163 | - clone server instance at each socket 164 | - on android : save the cfg in a persistant storage (can reinstall without loose) 165 | - runner accept a log parameter, default to False 166 | - manage ctrl-c 167 | - better runner detection in android/kivy 168 | - .server(port=8000) : can set a specific port in server mode, else 8000 169 | - refacto ws.on_message 170 | - evil script dict 171 | - no more "guy.use" 172 | - introduce self (current guy instance), js side ! 173 | - self != guy !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 174 | - no more guy.EXIT() 175 | 176 | changelog 0.2: 177 | 178 | - fetch ssl bypass 179 | - guy.EXIT() 180 | -------------------------------------------------------------------------------- /docs/howto_build_apk_android.md: -------------------------------------------------------------------------------- 1 | # How to build an APK for Android 2 | 3 | 4 | You will need to install [kivy](https://kivy.org/) and [buildozer](https://pypi.org/project/buildozer/) ! 5 | 6 | This How-to assume that you use a linux platform ;-) 7 | 8 | 9 | ## Install the tools 10 | 11 | ``` 12 | sudo apt install python3-kivy zipalign 13 | python3 -m pip install --upgrade buildozer 14 | ``` 15 | Note : 16 | 17 | - you should install the kivy version which belongs to your platform 18 | - For buildozer : you can pip it ! 19 | 20 | ## Create your first Guy's apk 21 | 22 | Create an empty folder, and from a console inside the folder 23 | 24 | Get `guy.py` module (needed, to be embedded in apk) 25 | 26 | wget https://raw.githubusercontent.com/manatlan/guy/master/guy.py 27 | 28 | Get an icon/splashscreen for the apk 29 | 30 | wget https://raw.githubusercontent.com/manatlan/guy/master/android/data/logo.png 31 | 32 | Create the file `buildozer.spec` ([specs](https://buildozer.readthedocs.io/en/latest/specifications.html)), with this content: 33 | 34 | [app] 35 | title = Guy Demo 36 | package.name = com.guy 37 | package.domain = demo 38 | source.dir = . 39 | source.include_exts = 40 | version = 0.1 41 | requirements = python3,kivy,tornado 42 | presplash.filename = %(source.dir)s/logo.png 43 | icon.filename = %(source.dir)s/logo.png 44 | orientation = portrait 45 | osx.python_version = 3 46 | osx.kivy_version = 1.9.1 47 | fullscreen = 0 48 | android.permissions = INTERNET 49 | android.api = 28 50 | android.ndk = 17c 51 | android.arch = arm64-v8a 52 | 53 | [buildozer] 54 | log_level = 2 55 | warn_on_root = 1 56 | 57 | (You can setup your [android.permissions](https://developer.android.com/reference/android/Manifest.permission.html) according your needs (separated by comma "`,`")) 58 | 59 | Create the file `main.py` (your file app should be named `main.py`, it's a buildozer's request): 60 | 61 | ```python 62 | from guy import Guy 63 | 64 | class Hello(Guy): 65 | __doc__="""<button onclick="self.test().then( function(x) {document.body.innerHTML+=x})">test</button>""" 66 | 67 | def test(self): 68 | return "hello world" 69 | 70 | if __name__ == "__main__": 71 | app=Hello() 72 | app.run() 73 | ``` 74 | 75 | Run the app in your environment ... to be sure it works as is 76 | 77 | python3 main.py 78 | 79 | Connect your smartphone with an usb cable to your computer (and allow `file transfer` mode in your android), and run: 80 | 81 | buildozer android debug deploy run 82 | 83 | First run is very long (more than 20min on my computer), second run is a lot faster (10sec) ... 84 | 85 | Your android will prompt you to authorize the installation : check yes ... 86 | 87 | Your app should start on the phone ;-) 88 | 89 | 90 | 91 | !!! info 92 | But, recent android, doesn't allow to use http traffic (error `ERR_CLEARTEXT_NOT_PERMITTED`). So you will need to authorize "Clear Text Traffic" for your APK. It's not a problem, or a security risk (the app will only listening http on localhost), see next section. 93 | 94 | 95 | 96 | ## Authorize "Clear Text Traffic" in your APK 97 | You will need to authorize your app to access the embedded python http server, which serve on localhost "http" only. To do that, you must enable "Clear Text Traffic" in your "AndroidManifest.xml". Using buildozer, you can change the template which will be used to generate the original. 98 | 99 | Open your file `.buildozer/android/platform/build/dists/<<package.name>>/templates/AndroidManifest.tmpl.xml` 100 | (.buildozer/android/platform/build/dists/com.guy/templates/AndroidManifest.tmpl.xml) 101 | 102 | Add `android:usesCleartextTraffic="true"` in tag `<application>` in `AndroidManifest.tmpl.xml` 103 | 104 | Search the tag `<application>` which look like this : 105 | 106 | ```xml 107 | <application android:label="@string/app_name" 108 | android:icon="@drawable/icon" 109 | android:allowBackup="{{ args.allow_backup }}" 110 | android:theme="@android:style/Theme.NoTitleBar{% if not args.window %}.Fullscreen{% endif %}" 111 | android:hardwareAccelerated="true" > 112 | ``` 113 | And change it to : 114 | ```xml hl_lines="3" 115 | <application android:label="@string/app_name" 116 | android:icon="@drawable/icon" 117 | android:usesCleartextTraffic="true" 118 | android:allowBackup="{{ args.allow_backup }}" 119 | android:theme="@android:style/Theme.NoTitleBar{% if not args.window %}.Fullscreen{% endif %}" 120 | android:hardwareAccelerated="true" > 121 | ``` 122 | 123 | !!! tip 124 | If you modify `buildozer.spec`, it can alter the manifest. So you will need to reproduce this step ! 125 | 126 | Alternatively, you can use this sed command to do it, in one line 127 | 128 | sed -i 's/<application android:label/<application android:usesCleartextTraffic="true" android:label/g' .buildozer/android/platform/build/dists/com.guy/templates/AndroidManifest.tmpl.xml 129 | 130 | ## Deploy in android's playstore 131 | 132 | You will need to sign your apk, before uploading it. You will need [OpenJDK tools](https://openjdk.java.net/tools/index.html) ! 133 | 134 | To release your apk: 135 | 136 | buildozer android release 137 | 138 | It will produce an apk file ... but the command ends with an error "FileNotFoundError: [Errno 2] No such file or directory ..."" 139 | 140 | In fact, the APK release is here : ".buildozer/android/platform/build/dists/com.guy/build/outputs/apk/release/com.guy-release-unsigned.apk" 141 | 142 | Just copy it, in the `bin` folder: 143 | 144 | cp .buildozer/android/platform/build/dists/com.guy/build/outputs/apk/release/com.guy-release-unsigned.apk bin/ 145 | 146 | To sign your APK, you will need to create your self-signed key ! 147 | 148 | keytool -genkey -v -keystore my-app.keystore -alias cb-play -keyalg RSA -keysize 2048 -validity 10000 149 | 150 | When you get your keystore (file `my-app.keystore `), you can sign the apk, by doing : 151 | 152 | jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ./my-app.keystore ./bin/com.guy-release-unsigned.apk cb-play 153 | 154 | When it's done, just [zipalign](https://developer.android.com/studio/command-line/zipalign) the apk, like that : 155 | 156 | zipalign -v 4 ./bin/com.guy-release-unsigned.apk ./bin/myapp.apk 157 | 158 | Now, your apk `myapp.apk` can be distributed, or uploaded to [playstore](https://play.google.com/apps/publish). 159 | 160 | !!! info 161 | Here is the [myapp.apk (~13Mo)](https://cdn.glitch.com/00392733-d07a-42ad-a17a-c0df9475b388%2Fmyapp.apk?v=1574618969557), that I have released when following this howto. Succesfully installed and tested on android9 & android10 ! And here is [this apk on the playstore](https://play.google.com/store/apps/details?id=demo.com.guy) ! 162 | 163 | 164 | 165 | 166 | ## Known Limitations 167 | 168 | - The **android's BACK KEY** does nothing ;-( (NEED TO IMPROVE THAT). You should provide a way to let the user quit your app (by calling `self.exit()`) 169 | - If you plan to use [vbuild](https://github.com/manatlan/vbuild) (to compile vue sfc components in html), to generate html. You can't use [PyComponents](https://github.com/manatlan/vbuild/blob/master/doc/PyComponent.md). And you will need vbuild >= 0.8.1. (the module [pscript](https://github.com/flexxui/pscript/issues/38#issuecomment-521960204) can't be embedded in an apk) 170 | - BTW, Some python modules can't be embedded in an APK : use pure python modules ! 171 | - When you use html in docstring in a guy class. You will need to prefix your docstring like this `__doc__="""html"""`. Because buildozer remove real docstrings from py files. 172 | - Don't try to embed a GuyApp which are runned by `app.runCef()` or `app.serve()` ... only `app.run()` will work ;-) 173 | 174 | 175 | ## Sources 176 | 177 | * [https://linuxfr.org/news/minipy-un-serveur-python-dans-son-android](https://linuxfr.org/news/minipy-un-serveur-python-dans-son-android) -------------------------------------------------------------------------------- /docs/server.md: -------------------------------------------------------------------------------- 1 | # Server Side : python guy 2 | 3 | Basically, you subclass the guy class like this: 4 | 5 | ```python 6 | #!/usr/bin/python3 -u 7 | from guy import Guy 8 | 9 | class Simple(Guy): 10 | size=(400,400) 11 | __doc__="""<button onclick="self.test()">test</button>""" 12 | 13 | def test(self): 14 | print("hello world") 15 | 16 | ``` 17 | And all declared methods will be available on [client side](client.md). 18 | 19 | Here there will be a `self.test()` method in client side. 20 | 21 | Understand that a guy's class is an html page. Declared methods will be available on client side, and will be directly usable from js side. 22 | 23 | 24 | ## Rendering the UI 25 | 26 | The rendering is done after the instanciation of the class : when the client connects, or do a refresh. 27 | 28 | ### Rendering with docstring 29 | It's the simplest thing : just declare your gui/html in the docstring of your class. 30 | 31 | ```python 32 | class Simple(Guy): 33 | __doc__="""<button onclick="self.test()">test</button>""" 34 | ``` 35 | 36 | It's a fast way to release a simple component. But it's not adapted for larger app ;-) 37 | 38 | !!! info 39 | here is the `__doc__` declaration. Which is needed if you want to release a component like that on android (because buildozer seems to remove them, if not prefixed) 40 | 41 | 42 | **TODO** : talk about template engine ! (`<<var>>` replaced by instance/class attributs) 43 | 44 | 45 | ### Rendering with an html file 46 | If you want to separate the UI from the code (best practice). You can put your html in a file named as the class name, in a `static` folder. 47 | 48 | It's the preferable way to go, for larger app. 49 | 50 | !!! info 51 | In this case : you should provide a tag `<script src="guy.js"></script>` in your html. 52 | 53 | **TODO** : talk about template engine ! (`<<var>>` replaced by instance/class attributs) 54 | 55 | 56 | ### Rendering override 57 | Sometimes, you need to make more things, and you can do it, by overriding the `render(self, path)` method of your class. 58 | 59 | For bigger app : I use [vbuild](https://github.com/manatlan/vbuild) to render vuejs/sfc components. 60 | (see [starter-guy-vuejs](https://glitch.com/~starter-guy-vuejs), and [demo](https://starter-guy-vuejs.glitch.me/#/)) 61 | 62 | ```python 63 | class App(Guy): 64 | 65 | def render(self,path): # override default 66 | with open( os.path.join(path,"app/APP.html") as fid: 67 | buf=fid.read() 68 | r=vbuild.render( os.path.join(path,"app/*.vue") ) 69 | 70 | buf=buf.replace("<!-- TPLS -->",r.html) 71 | buf=buf.replace("/* CSS */",r.style) 72 | buf=buf.replace("/* JS */", r.script) 73 | 74 | return buf 75 | ``` 76 | !!! info 77 | In this case : you should provide a tag `<script src="guy.js"></script>` in your response. 78 | 79 | 80 | ## Guy Class 81 | 82 | Like a normal class : you can override its constructor and set some attributs at this place. Note that, the constructor is called before the rendering : so it's the perfect place to setup some things before rendering. 83 | 84 | 85 | ***TODO*** : Talk about returning {script: "..."} 86 | 87 | ***TODO*** : Talk about sync/async methods. 88 | 89 | ###`init(self)` 90 | Override this method to do thing, when a client is connected. (init method can be sync or async) 91 | 92 | ###`render(self,path)` 93 | Override this method to override default rendering (see [Rendering override](#rendering-override)) 94 | 95 | `path` is the path to the data (where static folder sits) 96 | 97 | If this method is not overriden : it will try to get the html from the `__doc__`'s string, if not ... from the static folder (`guy.FOLDERSTATIC`) 98 | 99 | ###`self.exit( returnValue=None )` 100 | Exit the app. 101 | 102 | If you want to get a "returnValue", you can set the returned value, which will be returned in py side: 103 | 104 | ```python 105 | myapp=MyGuyApp() 106 | returnValue = myapp.run() 107 | ``` 108 | 109 | ###`cleanup( ...)` 110 | **NEW in 0.7.6** 111 | event to define for cleanup things. (*TODO: need better explain*) 112 | 113 | ###`afterServerStarted( ... )` 114 | **NEW in 0.7.6** 115 | event to define for starting things. (*TODO: need better explain*) 116 | 117 | ###`async self.emit( event, arg1, arg2 ... )` 118 | Call this method to emit an `event` to all connected clients. 119 | 120 | It's a non-sense in `app` or `cef` mode : because there is only one client. It only got sense in `server` mode. Prefer the following `emitMe` to send an event to the gui. 121 | 122 | ###`async self.emitMe( event, arg1, arg2 ... )` 123 | Call this method to emit an `event` to the connected client. 124 | 125 | ###`self.parent` 126 | This attribut contains the main instance (the one which starts all (which done `.run()`)). If it's `None`, you are in the main instance. 127 | 128 | ###`async self.js` 129 | With this wrapping object, you can call js method (from the connected client) from py side, and get back a returned value : 130 | 131 | ```python 132 | name = await self.js.prompt("what's your name ?") 133 | ``` 134 | Notes: 135 | 136 | - It can throw a `guy.JSException` if something is wrong on js side! 137 | - On py side; you'll need to await the call, even if you don't need to get back the returned value. 138 | - On js side; your js method can be sync or async. But your method needs to be in `window` scope. 139 | 140 | !!! info 141 | Only available for guy >= 0.4.3 142 | 143 | 144 | ###`self.cfg` 145 | A place to get/set vars, which will be stored on server side, in a `config.json` file, where the main executable is runned. 146 | (if the guy'app is embedded in a pip/package, the config file will be stored in ``~/.<package_name>.json`) 147 | 148 | To set a var 'name', in py side : 149 | 150 | ```python 151 | self.cfg.name = "Elton" 152 | ``` 153 | 154 | To get a var 'name', in py side : 155 | 156 | ```python 157 | name = self.cfg.name 158 | ``` 159 | 160 | ###`size` 161 | With this class attribut you can specify the size of your window. 162 | 163 | This is a non-sense, in `server` mode. Because, it's the client/browser which determine the size of its tab. 164 | 165 | This is a non-sense, when runned on android. Because the window is the full screen. 166 | 167 | ```python 168 | class Simple(Guy): 169 | """<button onclick="self.test()">test</button>""" 170 | size=(400,400) 171 | ``` 172 | 173 | ```python 174 | class Simple(guy.Guy): 175 | """<button onclick="self.test()">test</button>""" 176 | size=guy.FULLSCREEN 177 | ``` 178 | 179 | !!! info 180 | `Size` may be relevant for the main window (the first started). It has no effect after a navigation or for embedded window. 181 | 182 | 183 | ## Static content 184 | **guy** will serve everything that is contained in a `static` folder, as static content. 185 | 186 | It's really useful, when you don't embbed your html in the docstring of the class, or if you need 187 | static content like images, css files, etc ... 188 | 189 | !!! important 190 | - Static content should contain dots (".") in filename ! If not; current guy's version consider it must be served as dynamic content ! (it may change in the near future) 191 | 192 | 193 | This static folder should be in the same path as your main guy class, like this : 194 | 195 | ``` 196 | ├── main.py <- Contains class Index(guy.Guy) 197 | └── static 198 | └── Index.html 199 | ``` 200 | 201 | It's possible to use another for this folder, by setting `guy.FOLDERSTATIC = "ui"` at start. 202 | 203 | ## Hook http 204 | **Guy** provides an `http` decorator to handle specific http requests. It can be useful for a lot of things. 205 | 206 | ```python 207 | from guy import http 208 | 209 | @http("/item/(\d+)") 210 | def getItem(web,number): 211 | web.write( "item %s"%number ) 212 | ``` 213 | `web` is a [Tornado's RequestHandler](https://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler) 214 | 215 | **TODO** : talk about returning a guy window (for beautiful url) 216 | 217 | !!! important 218 | The url catched by the hook http, can't contain dots (".") ! If there is a dot; current guy's version consider it must be served as static content ! (it may change in the near future) -------------------------------------------------------------------------------- /android/buildozer.spec: -------------------------------------------------------------------------------- 1 | [app] 2 | title = guydemo 3 | 4 | # (str) Package name 5 | package.name = com.manatlan 6 | 7 | # (str) Package domain (needed for android/ios packaging) 8 | package.domain = guydemo2 9 | 10 | # (str) Source code where the main.py live 11 | source.dir = . 12 | 13 | # (list) Source files to include (let empty to include all the files) 14 | source.include_exts = 15 | 16 | # (list) List of inclusions using pattern matching 17 | #source.include_patterns = assets/*,images/*.png 18 | 19 | # (list) Source files to exclude (let empty to not exclude anything) 20 | #source.exclude_exts = spec 21 | 22 | # (list) List of directory to exclude (let empty to not exclude anything) 23 | #source.exclude_dirs = tests, bin 24 | 25 | # (list) List of exclusions using pattern matching 26 | #source.exclude_patterns = license,images/*/*.jpg 27 | 28 | # (str) Application versioning (method 1) 29 | version = 0.1 30 | 31 | # (str) Application versioning (method 2) 32 | # version.regex = __version__ = ['"](.*)['"] 33 | # version.filename = %(source.dir)s/main.py 34 | 35 | # (list) Application requirements 36 | # comma separated e.g. requirements = sqlite3,kivy 37 | requirements = python3,kivy,tornado 38 | 39 | # (str) Custom source folders for requirements 40 | # Sets custom source for any requirements with recipes 41 | # requirements.source.kivy = ../../kivy 42 | 43 | # (list) Garden requirements 44 | #garden_requirements = 45 | 46 | # (str) Presplash of the application 47 | presplash.filename = %(source.dir)s/data/splash.png 48 | 49 | # (str) Icon of the application 50 | icon.filename = %(source.dir)s/data/logo.png 51 | 52 | # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) 53 | orientation = portrait 54 | 55 | # (list) List of service to declare 56 | #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY 57 | 58 | # 59 | # OSX Specific 60 | # 61 | 62 | # 63 | # author = © Copyright Info 64 | 65 | # change the major version of python used by the app 66 | osx.python_version = 3 67 | 68 | # Kivy version to use 69 | osx.kivy_version = 1.9.1 70 | 71 | # 72 | # Android specific 73 | # 74 | 75 | # (bool) Indicate if the application should be fullscreen or not 76 | fullscreen = 0 77 | 78 | # (string) Presplash background color (for new android toolchain) 79 | # Supported formats are: #RRGGBB #AARRGGBB or one of the following names: 80 | # red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, 81 | # darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, 82 | # olive, purple, silver, teal. 83 | #android.presplash_color = #FFFFFF 84 | 85 | # (list) Permissions 86 | android.permissions = INTERNET,CAMERA,RECORD_AUDIO 87 | #,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE 88 | 89 | # (int) Target Android API, should be as high as possible. 90 | android.api = 28 91 | 92 | # (int) Minimum API your APK will support. 93 | #android.minapi = 21 94 | 95 | # (int) Android SDK version to use 96 | #android.sdk = 20 97 | 98 | # (str) Android NDK version to use 99 | android.ndk = 17c 100 | 101 | # (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. 102 | #android.ndk_api = 21 103 | 104 | # (bool) Use --private data storage (True) or --dir public storage (False) 105 | #android.private_storage = True 106 | 107 | # (str) Android NDK directory (if empty, it will be automatically downloaded.) 108 | #android.ndk_path = 109 | 110 | # (str) Android SDK directory (if empty, it will be automatically downloaded.) 111 | #android.sdk_path = 112 | 113 | # (str) ANT directory (if empty, it will be automatically downloaded.) 114 | #android.ant_path = 115 | 116 | # (bool) If True, then skip trying to update the Android sdk 117 | # This can be useful to avoid excess Internet downloads or save time 118 | # when an update is due and you just want to test/build your package 119 | # android.skip_update = False 120 | 121 | # (bool) If True, then automatically accept SDK license 122 | # agreements. This is intended for automation only. If set to False, 123 | # the default, you will be shown the license when first running 124 | # buildozer. 125 | # android.accept_sdk_license = False 126 | 127 | # (str) Android entry point, default is ok for Kivy-based app 128 | #android.entrypoint = org.renpy.android.PythonActivity 129 | 130 | # (list) Pattern to whitelist for the whole project 131 | #android.whitelist = 132 | 133 | # (str) Path to a custom whitelist file 134 | #android.whitelist_src = 135 | 136 | # (str) Path to a custom blacklist file 137 | #android.blacklist_src = 138 | 139 | # (list) List of Java .jar files to add to the libs so that pyjnius can access 140 | # their classes. Don't add jars that you do not need, since extra jars can slow 141 | # down the build process. Allows wildcards matching, for example: 142 | # OUYA-ODK/libs/*.jar 143 | #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar 144 | 145 | # (list) List of Java files to add to the android project (can be java or a 146 | # directory containing the files) 147 | #android.add_src = 148 | 149 | # (list) Android AAR archives to add (currently works only with sdl2_gradle 150 | # bootstrap) 151 | #android.add_aars = 152 | 153 | # (list) Gradle dependencies to add (currently works only with sdl2_gradle 154 | # bootstrap) 155 | #android.gradle_dependencies = 156 | 157 | # (list) Java classes to add as activities to the manifest. 158 | #android.add_activites = com.example.ExampleActivity 159 | 160 | # (str) python-for-android branch to use, defaults to master 161 | #p4a.branch = master 162 | 163 | # (str) OUYA Console category. Should be one of GAME or APP 164 | # If you leave this blank, OUYA support will not be enabled 165 | #android.ouya.category = GAME 166 | 167 | # (str) Filename of OUYA Console icon. It must be a 732x412 png image. 168 | #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png 169 | 170 | # (str) XML file to include as an intent filters in <activity> tag 171 | #android.manifest.intent_filters = 172 | 173 | # (str) launchMode to set for the main activity 174 | #android.manifest.launch_mode = standard 175 | 176 | # (list) Android additional libraries to copy into libs/armeabi 177 | #android.add_libs_armeabi = libs/android/*.so 178 | #android.add_libs_armeabi_v7a = libs/android-v7/*.so 179 | #android.add_libs_x86 = libs/android-x86/*.so 180 | #android.add_libs_mips = libs/android-mips/*.so 181 | 182 | # (bool) Indicate whether the screen should stay on 183 | # Don't forget to add the WAKE_LOCK permission if you set this to True 184 | #android.wakelock = False 185 | 186 | # (list) Android application meta-data to set (key=value format) 187 | #android.meta_data = 188 | 189 | # (list) Android library project to add (will be added in the 190 | # project.properties automatically.) 191 | #android.library_references = 192 | 193 | # (str) Android logcat filters to use 194 | #android.logcat_filters = *:S python:D 195 | 196 | # (bool) Copy library instead of making a libpymodules.so 197 | #android.copy_libs = 1 198 | 199 | # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86 200 | android.arch = armeabi-v7a 201 | 202 | # 203 | # Python for android (p4a) specific 204 | # 205 | 206 | # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) 207 | #p4a.source_dir = 208 | 209 | # (str) The directory in which python-for-android should look for your own build recipes (if any) 210 | #p4a.local_recipes = 211 | 212 | # (str) Filename to the hook for p4a 213 | #p4a.hook = 214 | 215 | # (str) Bootstrap to use for android builds 216 | # p4a.bootstrap = sdl2 217 | 218 | # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) 219 | #p4a.port = 220 | 221 | 222 | # 223 | # iOS specific 224 | # 225 | 226 | # (str) Path to a custom kivy-ios folder 227 | #ios.kivy_ios_dir = ../kivy-ios 228 | # Alternately, specify the URL and branch of a git checkout: 229 | ios.kivy_ios_url = https://github.com/kivy/kivy-ios 230 | ios.kivy_ios_branch = master 231 | 232 | # Another platform dependency: ios-deploy 233 | # Uncomment to use a custom checkout 234 | #ios.ios_deploy_dir = ../ios_deploy 235 | # Or specify URL and branch 236 | ios.ios_deploy_url = https://github.com/phonegap/ios-deploy 237 | ios.ios_deploy_branch = 1.7.0 238 | 239 | # (str) Name of the certificate to use for signing the debug version 240 | # Get a list of available identities: buildozer ios list_identities 241 | #ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)" 242 | 243 | # (str) Name of the certificate to use for signing the release version 244 | #ios.codesign.release = %(ios.codesign.debug)s 245 | 246 | 247 | [buildozer] 248 | 249 | # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) 250 | log_level = 2 251 | 252 | # (int) Display warning if buildozer is run as root (0 = False, 1 = True) 253 | warn_on_root = 1 254 | 255 | # (str) Path to build artifact storage, absolute or relative to spec file 256 | # build_dir = ./.buildozer 257 | 258 | # (str) Path to build output (i.e. .apk, .ipa) storage 259 | # bin_dir = ./bin 260 | 261 | # ----------------------------------------------------------------------------- 262 | # List as sections 263 | # 264 | # You can define all the "list" as [section:key]. 265 | # Each line will be considered as a option to the list. 266 | # Let's take [app] / source.exclude_patterns. 267 | # Instead of doing: 268 | # 269 | #[app] 270 | #source.exclude_patterns = license,data/audio/*.wav,data/images/original/* 271 | # 272 | # This can be translated into: 273 | # 274 | #[app:source.exclude_patterns] 275 | #license 276 | #data/audio/*.wav 277 | #data/images/original/* 278 | # 279 | 280 | 281 | # ----------------------------------------------------------------------------- 282 | # Profiles 283 | # 284 | # You can extend section / key with a profile 285 | # For example, you want to deploy a demo version of your application without 286 | # HD content. You could first change the title to add "(demo)" in the name 287 | # and extend the excluded directories to remove the HD content. 288 | # 289 | #[app@demo] 290 | #title = My Application (demo) 291 | # 292 | #[app:source.exclude_patterns@demo] 293 | #images/hd/* 294 | # 295 | # Then, invoke the command line with the "demo" profile: 296 | # 297 | #buildozer --profile demo android debug 298 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Davide Rosa 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /guy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ############################################################################# 4 | # Apache2 2019-2020 - manatlan manatlan[at]gmail(dot)com 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # more: https://github.com/manatlan/guy 17 | # ############################################################################# 18 | 19 | #python3.7 -m pytest --cov-report html --cov=guy . 20 | 21 | #TODO: 22 | # logger for each part 23 | # cookiejar 24 | 25 | 26 | __version__="0.7.6" 27 | 28 | import os,sys,re,traceback,copy,types,shutil 29 | from urllib.parse import urlparse 30 | from threading import Thread 31 | import tornado.web 32 | import tornado.websocket 33 | import tornado.platform.asyncio 34 | import tornado.autoreload 35 | import tornado.httpclient 36 | from tornado.websocket import websocket_connect 37 | from tornado.ioloop import IOLoop 38 | from tornado import gen 39 | import platform 40 | import json 41 | import asyncio 42 | import time 43 | import socket 44 | from datetime import datetime,date 45 | import tempfile 46 | import subprocess 47 | import webbrowser 48 | import concurrent 49 | import inspect 50 | import uuid 51 | import logging 52 | import io 53 | 54 | class FULLSCREEN: pass 55 | ISANDROID = "android" in sys.executable 56 | FOLDERSTATIC="static" 57 | CHROMECACHE=".cache" 58 | WSGUY=None # or "wss://example.com" (ws server) 59 | class JSException(Exception): pass 60 | 61 | handler = logging.StreamHandler() 62 | handler.setFormatter( logging.Formatter('-%(asctime)s %(name)s [%(levelname)s]: %(message)s') ) 63 | handler.setLevel(logging.ERROR) 64 | logger = logging.getLogger("guy") 65 | logger.addHandler(handler) 66 | logger.setLevel(logging.ERROR) 67 | 68 | 69 | def isFree(ip, port): 70 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 71 | s.settimeout(1) 72 | return not (s.connect_ex((ip,port)) == 0) 73 | 74 | #=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=# 75 | https={} 76 | def http(regex): # decorator 77 | if not regex.startswith("/"): raise Exception("http decoraton, path regex should start with '/'") 78 | def _(method): 79 | https["^"+regex[1:]+"$"] = method 80 | return _ 81 | 82 | async def callhttp(web,path): # web: RequestHandler 83 | for name,method in https.items(): 84 | g=re.match(name,path) 85 | if g: 86 | if asyncio.iscoroutinefunction( method ): 87 | ret=await method(web,*g.groups()) 88 | else: 89 | ret=method(web,*g.groups()) 90 | 91 | if isinstance(ret,Guy): 92 | ret.parent = web.instance 93 | web.write( ret._renderHtml() ) 94 | return True 95 | #=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=# 96 | 97 | def wsquery(wsurl,msg): # Synchrone call, with tornado 98 | """ In a simple world, could be (with websocket_client pypi): 99 | ws = websocket.create_connection(wsurl) 100 | ws.send(msg) 101 | resp=ws.recv() 102 | ws.close() 103 | return resp 104 | """ 105 | @gen.coroutine 106 | def fct(ioloop,u,content): 107 | cnx=yield websocket_connect(u) 108 | cnx.write_message(msg) 109 | resp=yield cnx.read_message() 110 | cnx.close() 111 | ioloop.stop() 112 | ioloop.response=resp 113 | 114 | ioloop = IOLoop.instance() 115 | fct(ioloop,wsurl,msg) 116 | ioloop.start() 117 | return ioloop.response 118 | 119 | class readTextFile(str): 120 | filename=None 121 | encoding=None 122 | def __new__(cls,fn:str): 123 | for e in ["utf8","cp1252"]: 124 | try: 125 | with io.open(fn,"r",encoding=e) as fid: 126 | obj=str.__new__(cls,fid.read()) 127 | obj.filename=fn 128 | obj.encoding=e 129 | return obj 130 | except UnicodeDecodeError: 131 | pass 132 | raise Exception("Can't read '%s'"%fn) 133 | 134 | def serialize(obj): 135 | def toJSDate(d): 136 | assert type(d) in [datetime, date] 137 | d = datetime(d.year, d.month, d.day, 0, 0, 0, 0) if type(d) == date else d 138 | return d.isoformat() + "Z" 139 | 140 | if isinstance(obj, (datetime, date)): 141 | return toJSDate(obj) 142 | if isinstance(obj, bytes): 143 | return str(obj, "utf8") 144 | if hasattr(obj, "__dict__"): 145 | return obj.__dict__ 146 | else: 147 | return str(obj) 148 | 149 | 150 | def unserialize(obj): 151 | if type(obj) == str: 152 | if re.search(r"^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z$", obj): 153 | return datetime.strptime(obj, "%Y-%m-%dT%H:%M:%S.%fZ") 154 | elif re.search(r"^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$", obj): 155 | return datetime.strptime(obj, "%Y-%m-%dT%H:%M:%SZ") 156 | elif type(obj) == list: 157 | return [unserialize(i) for i in obj] 158 | return obj 159 | 160 | 161 | def jDumps(obj): 162 | return json.dumps(obj, default=serialize) 163 | 164 | 165 | def jLoads(s): 166 | return unserialize( 167 | json.loads(s, object_pairs_hook=lambda obj: {k: unserialize(v) for k, v in obj}) 168 | ) 169 | 170 | 171 | class JDict: 172 | def __init__(self, f: str): 173 | self.__f = f 174 | try: 175 | with open(self.__f, "r+") as fid: 176 | self.__d = ( 177 | json.load( 178 | fid, 179 | object_pairs_hook=lambda obj: { 180 | k: unserialize(v) for k, v in obj 181 | }, 182 | ) 183 | or {} 184 | ) 185 | except FileNotFoundError as e: 186 | self.__d = {} 187 | 188 | def set(self, k: str, v): 189 | self.__d[k] = v 190 | self.__save() 191 | 192 | def get(self, k: str = None): 193 | return self.__d.get(k, None) if k else self.__d 194 | 195 | def __save(self): 196 | with open(self.__f, "w+") as fid: 197 | json.dump(self.__d, fid, indent=4, sort_keys=True, default=serialize) 198 | 199 | class GuyJSHandler(tornado.web.RequestHandler): 200 | def initialize(self, instance): 201 | self.instance=instance 202 | async def get(self,id): 203 | o=Guy._instances.get( id ) 204 | if o: 205 | self.write(o._renderJs(id)) 206 | else: 207 | raise tornado.web.HTTPError(status_code=404) 208 | 209 | 210 | class FavIconHandler(tornado.web.RequestHandler): 211 | def initialize(self, instance): 212 | self.instance=instance 213 | async def get(self): 214 | self.write(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\x00\x00\x00\x06bKGD\x00\xd4\x00{\x00\xff\xf0\x90\n\xda\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xe4\x04\x0e\x0f)\x02\xf5J\x9b=\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x06RIDATx\xda\xdd\x9b_\x88Te\x14\xc0\x7f\xe7\x9bu\xcd\x08D\x82\xfe=\xf4\x92\x88\xceV\xfe\x99\x9d\x82z(0{\x8a\x08\xa2\x07\xa9\xb0Q\x14\xb7\xb2\xcdXVLb\xd42)\x82\xb0\x82\xc5\xd5\x1a,\xd9\xe7\x02{)\x08|*[w\xdd\x9d\xd04\t\x0c\xa2\x1e\x82\xd0(1\xd7\x9d\xd3\xc3\xfc\xd9;3\xf7\xde\xef\xbbw\xef\xfc\xd9.\x0c\xbb;\xf7\xfbs\xbe\xdfw\xce\xf9\xce9\xf7\xae\xd0\x82\xeb\xccKz\'p\x0fp7p\x17p\xbb\x96\xb8\x15X\n\xdc\x02\xdc\x0c\xf4\x02=\x80\x00\x8arCK\\\x07\xae\x02\x7f\x03\x7f\x01\x7f\x02\x7f\x00\xbf\x03\xbf\x02\x972\xa3\xf2\xb3w\xae\xf1MJ\xf6\x98\xc4\x965V\xcf\xd3[\x95\xfe#\xe5\xaegvh\x1f%\xb2\xc0\xa3\xc0\xc3\xc0r\xbf>\xaa\x80Z\x06V\xd0\x92\x83\x00%\xae\x00\xdf\x02\'\x81S\xc0\xf7\x99\xa3\xf2O[\x00L\rj/\xb0^K\xe4\x80g<BY/um\xa3N\x10\x1a\x01\x9fG9\x0c|\xd9\xff\x89\\\x048\x9dS\xfa\x0b\x92\x0c\x80\xa9A]\x05\x0c\x03/\x04\xeeh\xe7!T\xfb\xfd\x06\xbc\xcc\x0c_\xf4\x7f&\xa5\xf1\x9c\x92\r\x00!!\x0bf\xcd!ajPW\x02c\xc0Z\'\xb5\xee4\x84f(\xcfg\x0br<\xb2\x06L\xbf\xaa\x8bU9\n<\x17*\xe8\xc2\x800\x0b\xdc\x9b-\xc8\xf9Fm0u\x8b\xde\xa9\xd5\x9f+\x10\xae\x89\t_<\x80\x88\x0fF\xe3\xe0|\\\xdbHtO\xd68\xb6\x08)\xe0\xc7\xf1\x9cn\xcd\x16\x84\xf1\x9c6\x8bZ\x1cRV\xbf/L\xef\xd4\r\xc0\x85H\x82\xfa\t\x99$\x04\x17\x00v\x08\x00\xa3\xe39=\xe0\x85P\xd7m\xfa5\xdd\x00|\xe5\xa7v\x1aW\xad\x932\x87Y\x07\x10>\xe6\xe8\x1d[\xe7\xee\xbd\x98-\xc8H\x1d\x80\xe2\x90.\x07.\xd6:\xfc\xff!\xac\xcc\x16\xe4\x82)\x0ekUeO\xd5\xa9\x8e$\xa8\xb2\x92\x909\xa4\x9a\xbe\x9a\x01\x0e\x00kQ\x1e\x00>\xb4\x99\x83\xc7\\?\xae\x89V\x1c\xd6-\xc0Q\xefn-\x00M\xc8\xd3\xcb\xfe\xccG\xf5\xab\x9d\xd8\xa6\x068\x82\xb2\xd9A\x13VT\xd9|\xd0\xb8[-\xd1\x04\x93\x98&\xe43\xa3\xb2\x9f\x99\xe6{\x99Q)eFe\x0b\xc2X\xd8\xd8" \xc2\x93\xa68\xac\x0fU\x92\x93&\x8f\xda\xa5\x10~Z7"\xfb\'\x07\x94\xcca\x7f\xdb\x9a\x1cP2\xa3\xf2,\x86k\x96\xb1\xef3\xc0\xe3a\xc7J\x17B(\x00\xac\x1b\tv,\x9e{\x9f\xfb\xcd\xe5\x19\xfb&\x03\xac\xb6\x9d\xad]\x06\xe1d\x84\xdcm,h\xae\xca\xd87\x0cp\x9bK\x80\x11\x06!\xb6\x97\x8f\xd7o&\x02\x80\x7f\xc3\x80\x8b)\x7f=\xe3\x1ae\xd5\x8e\x10q\x08\x87]\x16\xe3\xd8\xaf\xa1\xcd\x83\x93\x03\xf6\x04ab\x9b\x02<e\xd1:5\x92r *s\x9fN@h\x18;\x17f\xff\x9e\xd3\x00`\x93\xc5\xf4\xd4\x04\x04\x18\xdd\x0c!39\xa0\x1b-;\xcf\xc46\xddSw\xba\x05@0!QV(\x04:\x0balr@\x9f\x06\x98\xd8\xaeM;?9\xa0;\x80\xb7B\x07+\xcf\xaf\xf2\xc3n\xfd\x1ax,R\xbc]\xf2\xa9\x05h\xf4\x1a\xa0\xfaE\x87.\xfd\xe6\xda\x9c\x04F\x80\xe9J\x81\xb5\x1f\xd8\x01\xacs\xac/\x1e\xeb\xf1\x8b\xb7\xad\x10LYp\x91\x8a\x1cJ\xb5\xb6[\xb7[\xaav\x85R\xd3\x00A\xec\x00<c?R\xf9\x04kk\xf8Xj\x1c\x93\x8e@;\x920s0nV\xd5\xc9\x82\x8a\x99W!")\x08\x9d\xab*\xa9\t\xdb\x9a\xc8\x10Xx\x10\x8cM?#A0\x0b\x0e\x82\x1a\'#\x956Ch_}Q]\xf6\xd7\xbd:k\xec\xc2tA-\xa1\x0e@\x8fkV!\xc6\xb1N_="\xab\xed}\x8e\xa2\xda\xbd\xf2u9t>"\x1e\x91\xc6~\xfe{\x8e\xfa\xab\xce\x00Z\x04av\xcd!YF\x07/\x13\xb5Cls\x08\xa8%L\r*\x0b\n@\xd2\x10:}\x191\x01\x9e7\x01\x87\xe3]\xb4+\x84\xe2P{5\xa2\xa7v\xdec\x8f\xdd}\x9d\x89\xcd\'\xc8\\\x92\xe3\xe7\x13\x1aA\xde\xff\x9eP\x1c\xd2\xab\x18\xae;\xc9\xe0\xf5/\x12\x96u\xf9:GO2\x14\x17\x82\x99?\x04\x1f\xd3Y\x02,q\x99_\x03\xb2\xd1\xc0\x0c\xb6\xfey\xc7\x12\xd3\x14\xf9\xb5\xca\x1c\x82\xea\x8bI\xf9\x05\x89\xe1\x8f\x04LS\xc7\xb8\x10R\x11!t\xc2!\xfa\x04j\xc6w\x17b\n\x17\xbb\xb4\x96\x90\xa6\xc5\xc9[L\x90*\x8ai\x0b\x04\x89t\x9a\x88e\xfc\x18\xc9\x9b\tKz\xda\x00a&\xce\xee[!D\x88Q\x8c-\xf3k1\x04\xb5\x86b\x01\xbb\x1e*\x97q\x87`\\\xaa\xb3-\x85\x10\xd7\xee\xc5\x0e\xc1\xc5\x97\x19\xdfI\xba\x04\x82\xb5\xaf\x84\x07\xf3\x91j\x82\xdd\x06\xc1\xb5\x8f\xcc\x13\x82\t]`\x92\x10L\xeb\x80\xcd\x07\x82\xb1\n*\xf3[L\xe4#*\xae\xb9\x88\xc3\x06\x88?\x80\x1b\xd6\xb0\xd2D\x9f\xb0\x13\x10\xe2\xa4\xf1=\x94\xdf\xcboZ\x9c6&\x18~Op\\\x92\x90\x00\x08\x95\xa4D\x80\xa5\xc5a\xf5\xaa\xfd\x95VC\xf0>\xf9\xea\xa1\xfcf5q \xf8\xb6\x89\x10\xd5\xa1,\x06.\'q\\FN\xe3g\xe7\x96t!\xd4\xae,\xe6 13\xba\xaa:\xb6{\xf1\x8d\x8e\xd6\x00\xe3V\xe7\xd2J\x08\x1d\xbc$\x05\xa6o\x9fL8y\xd8\x16AH4\xe7\x8f\x9b!K\x8a\xe3\xb1\x8e\x996C\x10K\xa2\x13G\xa3*\xefIp(\xa9\'6-\x83\xe0XE\x8a\n\xc1\x00\xa4\xf3r\x1a\xf8.I\x08IV|\xc4\xd4\xafY\x12\x84\xe0m\xba\xd1\xb5\xb3\xd3\xb3\xbb\x84 \x04\xc9\x93\x14\x84Z\xb3t^.\x01\x9b\xbb\t\x82-,o\x82\x10#o\xa959\xf7\xa6\x92\xceK\x01x\xb7\x1b \x04\xbeY\x1a\x06!F\xf26\xa7\x01o\x08\xe7\xf6)\xe9\xbc\xec\x02\xf6\xcc\x0b\x82_\xa5y\xbe\x89\x8d\xb1@\x88\x99\xbc\xd5\xddJ\xe7\x85\xb3{\x95t^\xde\x06\xd6\xc7\x86\x10\xf3M\x0f\xd7\x98\xc3\xba\xd3\xc6\x1dB\xd3\xd7}{\x85\xf3\xef\x94H\xe7\xe5\x1bJ\xf4\x02\x07\x93\x82`-kG\x01g;n\x1d!\x98\xdd\x9ch\xfar\xe5\xaeZ\xc9t&\x9d\x97\xd7)\xbfr\xfa\n\xf0K\xab 8G\x9b!\xe3\x8bD\x87 \x00\xbb9!\x07y"0\xa7;\x9bW\xfa\xf6I\xf5\xf7;*\xe6\x91\x05VQ\xfe\x17\xf9e\xc0\xa2j{ux\xfb\xb3\xb1M\x9d`\n\xa4\xea\xfe\x9a\x05\xae\x89!\x05\xa4\x9a\xc6\xaf\xfc-=,\xd2RSnj\xfc\xdeF\xad<S\xfc\xf4?\xd9\xf1\xf7\xdeE\\\xb8\xa0\x00\x00\x00\x00IEND\xaeB`\x82') 215 | 216 | class MainHandler(tornado.web.RequestHandler): 217 | 218 | def initialize(self, instance): 219 | self.instance=instance 220 | async def get(self,page): # page doesn't contains a dot '.' 221 | ##################################################### 222 | if not await callhttp(self,page): 223 | ##################################################### 224 | if page=="" or page==self.instance._name: 225 | logger.debug("MainHandler: Render Main Instance (%s)",self.instance._name) 226 | self.write( self.instance._renderHtml() ) 227 | else: 228 | chpage=self.instanciate(page) # auto-instanciate each time ! 229 | chpage.parent = self.instance 230 | if chpage: 231 | logger.debug("MainHandler: Render Children (%s)",page) 232 | self.write( chpage._renderHtml() ) 233 | else: 234 | raise tornado.web.HTTPError(status_code=404) 235 | 236 | async def post(self,page): # page doesn't contains a dot '.' 237 | await self._callhttp(page) 238 | async def put(self,page): # page doesn't contains a dot '.' 239 | await self._callhttp(page) 240 | async def delete(self,page): # page doesn't contains a dot '.' 241 | await self._callhttp(page) 242 | async def options(self,page): # page doesn't contains a dot '.' 243 | await self._callhttp(page) 244 | async def head(self,page): # page doesn't contains a dot '.' 245 | await self._callhttp(page) 246 | async def patch(self,page): # page doesn't contains a dot '.' 247 | await self._callhttp(page) 248 | 249 | def instanciate(self,page): 250 | declared = {cls.__name__:cls for cls in Guy.__subclasses__()} 251 | gclass=declared[page] 252 | logger.debug("MainHandler: Auto instanciate (%s)",page) 253 | x=inspect.signature(gclass.__init__) 254 | args=[self.get_argument(i) for i in list(x.parameters)[1:]] 255 | return gclass(*args) 256 | 257 | async def _callhttp(self,page): 258 | if not await callhttp(self,page): 259 | raise tornado.web.HTTPError(status_code=404) 260 | 261 | 262 | 263 | class ProxyHandler(tornado.web.RequestHandler): 264 | def initialize(self, instance): 265 | self.instance=instance 266 | async def get(self,**kwargs): 267 | await self._do("GET",None,kwargs) 268 | async def post(self,**kwargs): 269 | await self._do("POST",self.request.body,kwargs) 270 | async def put(self,**kwargs): 271 | await self._do("PUT",self.request.body,kwargs) 272 | async def delete(self,**kwargs): 273 | await self._do("DELETE",self.request.body,kwargs) 274 | 275 | async def _do(self,method,body,qargs): 276 | url = str(qargs.get('url')) 277 | if not urlparse(url.lower()).scheme: 278 | url="http://%s:%s/%s"% (self.instance._webserver[0],self.instance._webserver[1],url.lstrip("/")) 279 | 280 | if self.request.query: 281 | url = url + "?" + self.request.query 282 | headers = {k[4:]: v for k, v in self.request.headers.items() if k.lower().startswith("set-")} 283 | 284 | http_client = tornado.httpclient.AsyncHTTPClient() 285 | logger.debug("PROXY FETCH (%s %s %s %s)",method,url,headers,body) 286 | try: 287 | response = await http_client.fetch(url, method=method,body=body,headers=headers,validate_cert = False) 288 | self.set_status(response.code) 289 | for k, v in response.headers.items(): 290 | if k.lower() in ["content-type", "date", "expires", "cache-control"]: 291 | self.set_header(k,v) 292 | logger.debug("PROXY FETCH, return=%s, size=%s",response.code,len(response.body)) 293 | self.write(response.body) 294 | except Exception as e: 295 | logger.debug("PROXY FETCH ERROR (%s), return 0",e) 296 | self.set_status(0) 297 | self.write(str(e)) 298 | 299 | 300 | async def sockwrite(theSock, **kwargs ): 301 | if theSock: 302 | try: 303 | await theSock.write_message(jDumps(kwargs)) 304 | except Exception as e: 305 | logger.error("Socket write : can't:%s",theSock) 306 | if theSock in WebSocketHandler.clients: del WebSocketHandler.clients[theSock] 307 | 308 | 309 | async def emit(event,*args): 310 | logger.debug(">>> Emit ALL: %s (%s)",event,args) 311 | for i in list( WebSocketHandler.clients.keys() ): 312 | await sockwrite(i,event=event,args=args) 313 | 314 | 315 | class WebSocketHandler(tornado.websocket.WebSocketHandler): 316 | clients={} 317 | returns={} 318 | 319 | def initialize(self, instance): 320 | self.instance=instance 321 | 322 | def open(self,id): 323 | o=Guy._instances.get( id ) 324 | if o: 325 | logger.debug("Connect %s",id) 326 | 327 | async def doInit( instance ): 328 | init=instance._getRoutage("init") 329 | if init: 330 | if asyncio.iscoroutinefunction( init ): 331 | await instance(self,"init") 332 | else: 333 | instance(self,"init") 334 | 335 | asyncio.ensure_future( doInit(o) ) 336 | 337 | WebSocketHandler.clients[self]=o 338 | 339 | def on_close(self): 340 | try: 341 | current = WebSocketHandler.clients[self] 342 | logger.debug("Disconnect %s", current._id) 343 | 344 | async def doCleanUp(instance): 345 | cleanup = instance._getRoutage("cleanup") 346 | if cleanup: 347 | if asyncio.iscoroutinefunction(cleanup): 348 | await instance(self, "cleanup") 349 | else: 350 | instance(self, "cleanup") 351 | 352 | asyncio.ensure_future(doCleanUp(current)) 353 | del WebSocketHandler.clients[self] 354 | except KeyError: 355 | pass 356 | 357 | async def on_message(self, message): 358 | current = WebSocketHandler.clients.get(self,None) 359 | if current is None: 360 | return 361 | 362 | o = jLoads(message) 363 | logger.debug("WS RECEPT: %s",o) 364 | method,args,uuid = o["command"],o.get("args"),o["uuid"] 365 | 366 | if method == "emit": 367 | event, *args = args 368 | await emit( event, *args ) # emit all 369 | elif method == "return": 370 | logger.debug(" as JS Response %s : %s",uuid,args) 371 | WebSocketHandler.returns[uuid]=args 372 | else: 373 | async def execution(function, uuid,mode): 374 | logger.debug(" as Execute (%s) %s(%s)",mode,method,args) 375 | try: 376 | ret = await function() 377 | ############################################################## 378 | if type(ret)==dict and "script" in ret: #evil mode 379 | s=ret["script"] 380 | del ret["script"] 381 | r = dict(result=ret,script=s, uuid=uuid) #evil mode 382 | else: 383 | ############################################################## 384 | r = dict(result=ret, uuid=uuid) 385 | except concurrent.futures._base.CancelledError as e: 386 | r = dict(error="task cancelled", uuid=uuid) 387 | except Exception as e: 388 | r = dict(error=str(e), uuid=uuid) 389 | logger.error("================================= in %s %s", method, mode) 390 | logger.error(traceback.format_exc().strip()) 391 | logger.error("=================================") 392 | logger.debug(">>> (%s) %s",mode,r) 393 | await sockwrite(self,**r) 394 | 395 | fct=current._getRoutage(method) 396 | 397 | if asyncio.iscoroutinefunction( fct ): 398 | 399 | async def function(): 400 | return await current(self,method,*args) 401 | 402 | #asyncio.create_task( execution( function, uuid, "ASYNC") ) #py37 403 | asyncio.ensure_future ( execution( function, uuid, "ASYNC") ) #py35 404 | 405 | else: 406 | async def function(): 407 | return current(self,method,*args) 408 | 409 | await execution( function, uuid, "SYNC" ) 410 | 411 | def check_origin(self, origin): 412 | return True 413 | 414 | 415 | class WebServer(Thread): # the webserver is ran on a separated thread 416 | port = 39000 417 | def __init__(self,instance,host="localhost",port=None,autoreload=False): 418 | super(WebServer, self).__init__() 419 | self.app=None 420 | self.instance=instance 421 | self.host=host 422 | self.autoreload=autoreload 423 | 424 | if port is not None: 425 | self.port = port 426 | 427 | while not isFree("localhost", self.port): 428 | self.port += 1 429 | 430 | self.instance._webserver=(self.host,self.port) 431 | self.instance._tornado = self 432 | 433 | try: # https://bugs.python.org/issue37373 FIX: tornado/py3.8 on windows 434 | if sys.platform == 'win32': 435 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 436 | except: 437 | pass 438 | 439 | def run(self): 440 | statics = os.path.join( self.instance._folder, FOLDERSTATIC) 441 | 442 | asyncio.set_event_loop(asyncio.new_event_loop()) 443 | tornado.platform.asyncio.AsyncIOMainLoop().install() 444 | if self.autoreload: 445 | print("**AUTORELOAD**") 446 | tornado.autoreload.start() 447 | tornado.autoreload.watch( sys.argv[0] ) 448 | if os.path.isdir(statics): 449 | for p in os.listdir( statics ) : 450 | tornado.autoreload.watch(os.path.abspath(os.path.join(statics, p))) 451 | 452 | self.app=tornado.web.Application([ 453 | (r'/_/(?P<url>.+)', ProxyHandler,dict(instance=self.instance)), 454 | (r'/(?P<id>[^/]+)-ws', WebSocketHandler,dict(instance=self.instance)), 455 | (r'/(?P<id>[^/]+)-js', GuyJSHandler,dict(instance=self.instance)), 456 | (r'/(?P<page>[^\.]*)', MainHandler,dict(instance=self.instance)), 457 | (r'/favicon.ico', FavIconHandler,dict(instance=self.instance)), 458 | (r'/(.*)', tornado.web.StaticFileHandler, dict(path=statics )) 459 | ], compress_response=True) 460 | self.app.listen(self.port,address=self.host) 461 | 462 | self.loop=asyncio.get_event_loop() 463 | try: 464 | if hasattr(self.instance,"afterServerStarted"): 465 | self.instance.afterServerStarted(self, None) 466 | except Exception as err: 467 | logger.error(f"Error in afterServerStarted Event: {str(err)}") 468 | 469 | async def _waitExit(): 470 | while self._exit==False: 471 | await asyncio.sleep(0.1) 472 | 473 | self._exit=False 474 | self.loop.run_until_complete(_waitExit()) 475 | 476 | # gracefull death 477 | try: 478 | tasks = asyncio.all_tasks(self.loop) #py37 479 | except: 480 | tasks = asyncio.Task.all_tasks(self.loop) #py35 481 | for task in tasks: task.cancel() 482 | try: 483 | self.loop.run_until_complete(asyncio.gather(*tasks)) 484 | except concurrent.futures._base.CancelledError: 485 | pass 486 | 487 | def exit(self): 488 | self._exit=True 489 | 490 | @property 491 | def startPage(self): 492 | return "http://localhost:%s/#%s" % (self.port,self.instance._name) #anchor is important ! (to uniqify ressource in webbrowser) 493 | 494 | 495 | 496 | class ChromeApp: 497 | def __init__(self, url, appname="driver",size=None,lockPort=None,chromeargs=[]): 498 | 499 | def find_chrome_win(): 500 | import winreg # TODO: pip3 install winreg 501 | 502 | reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe" 503 | for install_type in winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE: 504 | try: 505 | with winreg.OpenKey(install_type, reg_path, 0, winreg.KEY_READ) as reg_key: 506 | return winreg.QueryValue(reg_key, None) 507 | except WindowsError: 508 | pass 509 | 510 | def find_chrome_mac(): 511 | default_dir = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" 512 | if os.path.exists(default_dir): 513 | return default_dir 514 | 515 | 516 | if sys.platform[:3] == "win": 517 | exe = find_chrome_win() 518 | elif sys.platform == "darwin": 519 | exe = find_chrome_mac() 520 | else: 521 | for i in ["chromium-browser", "chromium", "google-chrome", "chrome"]: 522 | try: 523 | exe = webbrowser.get(i).name 524 | break 525 | except webbrowser.Error: 526 | exe = None 527 | 528 | if not exe: 529 | raise Exception("no chrome browser, no app-mode !") 530 | else: 531 | args = [ #https://peter.sh/experiments/chromium-command-line-switches/ 532 | exe, 533 | "--app=" + url, # need to be a real http page ! 534 | "--app-id=%s" % (appname), 535 | "--app-auto-launched", 536 | "--no-first-run", 537 | "--no-default-browser-check", 538 | "--disable-notifications", 539 | "--disable-features=TranslateUI", 540 | #~ "--no-proxy-server", 541 | ] + chromeargs 542 | if size: 543 | if size == FULLSCREEN: 544 | args.append("--start-fullscreen") 545 | else: 546 | args.append( "--window-size=%s,%s" % (size[0],size[1]) ) 547 | 548 | if lockPort: #enable reusable cache folder (coz only one instance can be runned) 549 | self.cacheFolderToRemove=None 550 | args.append("--remote-debugging-port=%s" % lockPort) 551 | args.append("--disk-cache-dir=%s" % CHROMECACHE) 552 | args.append("--user-data-dir=%s/%s" % (CHROMECACHE,appname)) 553 | else: 554 | self.cacheFolderToRemove=os.path.join(tempfile.gettempdir(),appname+"_"+str(os.getpid())) 555 | args.append("--user-data-dir=" + self.cacheFolderToRemove) 556 | args.append("--aggressive-cache-discard") 557 | args.append("--disable-cache") 558 | args.append("--disable-application-cache") 559 | args.append("--disable-offline-load-stale-cache") 560 | args.append("--disk-cache-size=0") 561 | 562 | logger.debug("CHROME APP-MODE: %s"," ".join(args)) 563 | # self._p = subprocess.Popen(args) 564 | self._p = subprocess.Popen(args,stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 565 | 566 | #~ if lockPort: 567 | #~ http_client = tornado.httpclient.HTTPClient() 568 | #~ self._ws = None 569 | #~ while self._ws == None: 570 | #~ try: 571 | #~ url = http_client.fetch("http://localhost:%s/json" % debugport).body 572 | #~ self._ws = json.loads(url)[0]["webSocketDebuggerUrl"] 573 | #~ except Exception as e: 574 | #~ self._ws = None 575 | 576 | def wait(self): 577 | self._p.wait() 578 | 579 | def __del__(self): # really important ! 580 | self._p.kill() 581 | if self.cacheFolderToRemove: shutil.rmtree(self.cacheFolderToRemove, ignore_errors=True) 582 | 583 | #~ def _com(self, payload: dict): 584 | #~ """ https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-close """ 585 | #~ payload["id"] = 1 586 | #~ r=json.loads(wsquery(self._ws,json.dumps(payload)))["result"] 587 | #~ return r or True 588 | 589 | #~ def focus(self): # not used 590 | #~ return self._com(dict(method="Page.bringToFront")) 591 | 592 | #~ def navigate(self, url): # not used 593 | #~ return self._com(dict(method="Page.navigate", params={"url": url})) 594 | 595 | def exit(self): 596 | #~ self._com(dict(method="Browser.close")) 597 | self._p.kill() 598 | 599 | 600 | 601 | class CefApp: 602 | def __init__(self, url, size=None, chromeArgs=None,lockPort=None): # chromeArgs is not used 603 | import pkgutil 604 | 605 | assert pkgutil.find_loader("cefpython3"), "cefpython3 not available" 606 | 607 | def cefbrowser(): 608 | from cefpython3 import cefpython as cef 609 | import ctypes 610 | 611 | isWin = platform.system() == "Windows" 612 | 613 | windowInfo = cef.WindowInfo() 614 | windowInfo.windowName = "Guy-CefPython3" 615 | if type(size) == tuple: 616 | w, h = size[0], size[1] 617 | windowInfo.SetAsChild(0, [0, 0, w, h]) # not win 618 | else: 619 | w, h = None, None 620 | 621 | sys.excepthook = cef.ExceptHook 622 | 623 | settings = { 624 | "product_version": "Guy/%s" % __version__, 625 | "user_agent": "Guy/%s (%s)" % (__version__, platform.system()), 626 | "context_menu": dict( 627 | enabled=True, 628 | navigation=False, 629 | print=False, 630 | view_source=False, 631 | external_browser=False, 632 | devtools=True, 633 | ), 634 | } 635 | if lockPort: 636 | settings["remote_debugging_port"]=lockPort 637 | settings["cache_path"]= CHROMECACHE 638 | 639 | cef.Initialize(settings, {}) 640 | b = cef.CreateBrowserSync(windowInfo, url=url) 641 | 642 | if isWin and w and h: 643 | window_handle = b.GetOuterWindowHandle() 644 | SWP_NOMOVE = 0x0002 # X,Y ignored with SWP_NOMOVE flag 645 | ctypes.windll.user32.SetWindowPos( 646 | window_handle, 0, 0, 0, w, h, SWP_NOMOVE 647 | ) 648 | 649 | # ===--- 650 | def guyInit(width, height): 651 | if size == FULLSCREEN: 652 | if isWin: 653 | b.ToggleFullscreen() # win only 654 | else: 655 | b.SetBounds(0, 0, width, height) # not win 656 | 657 | bindings = cef.JavascriptBindings() 658 | bindings.SetFunction("guyInit", guyInit) 659 | b.SetJavascriptBindings(bindings) 660 | 661 | b.ExecuteJavascript("guyInit(window.screen.width,window.screen.height)") 662 | # ===--- 663 | 664 | class GuyClientHandler(object): 665 | def OnLoadEnd(self, browser, **_): 666 | pass # could serve in the future (?) 667 | 668 | class GuyDisplayHandler(object): 669 | def OnTitleChange(self, browser, title): 670 | try: 671 | cef.WindowUtils.SetTitle(browser, title) 672 | except AttributeError: 673 | logger.warning( 674 | "**WARNING** : title changed '%s' not work on linux",title 675 | ) 676 | 677 | b.SetClientHandler(GuyClientHandler()) 678 | b.SetClientHandler(GuyDisplayHandler()) 679 | logger.debug("CEFPYTHON : %s",url) 680 | return cef 681 | 682 | self.__instance=cefbrowser() 683 | 684 | def wait(self): 685 | self.__instance.MessageLoop() 686 | 687 | def exit(self): 688 | self.__instance.Shutdown() 689 | 690 | 691 | 692 | 693 | def chromeBringToFront(port): 694 | if not isFree("localhost", port): 695 | http_client = tornado.httpclient.HTTPClient() 696 | url = http_client.fetch("http://localhost:%s/json" % port).body 697 | wsurl= json.loads(url)[0]["webSocketDebuggerUrl"] 698 | wsquery(wsurl,json.dumps(dict(id=1,method="Page.bringToFront"))) 699 | return True 700 | 701 | class LockPortFile: 702 | def __init__(self,name): 703 | self._file = os.path.join(CHROMECACHE,name,"lockport") 704 | 705 | def bringToFront(self): 706 | if os.path.isfile(self._file): # the file is here, perhaps it's running 707 | with open(self._file,"r") as fid: 708 | port=fid.read() 709 | 710 | if not isFree("localhost", int(port)): # if port is taken, perhaps it's running 711 | http_client = tornado.httpclient.HTTPClient() 712 | url = http_client.fetch("http://localhost:%s/json" % port).body 713 | wsurl= json.loads(url)[0]["webSocketDebuggerUrl"] 714 | print("*** ALREADY RUNNING") 715 | wsquery(wsurl,json.dumps(dict(id=1,method="Page.bringToFront"))) 716 | return True 717 | 718 | 719 | def create(self) -> int: 720 | if os.path.isfile(self._file): 721 | os.unlink(self._file) 722 | # find a freeport 723 | port=9990 724 | while not isFree("localhost", port): 725 | port += 1 726 | 727 | if not os.path.isdir( os.path.dirname(self._file)): 728 | os.makedirs( os.path.dirname(self._file) ) 729 | with open(self._file,"w") as fid: 730 | fid.write(str(port)) 731 | 732 | return port 733 | 734 | 735 | 736 | class GuyBase: 737 | def run(self,log=False,autoreload=False,one=False,args=[]): 738 | """ Run the guy's app in a windowed env (one client)""" 739 | self._log=log 740 | if log: 741 | handler.setLevel(logging.DEBUG) 742 | logger.setLevel(logging.DEBUG) 743 | 744 | if ISANDROID: #TODO: add executable for kivy/iOs mac/apple 745 | runAndroid(self) 746 | else: 747 | lockPort=None 748 | if one: 749 | lp=LockPortFile(self._name) 750 | if lp.bringToFront(): 751 | return 752 | else: 753 | lockPort = lp.create() 754 | 755 | ws=WebServer( self, autoreload=autoreload ) 756 | ws.start() 757 | 758 | app=ChromeApp(ws.startPage,self._name,self.size,lockPort=lockPort,chromeargs=args) 759 | 760 | self.RETOUR=None 761 | def exit(v=None): 762 | self.RETOUR=v 763 | 764 | ws.exit() 765 | app.exit() 766 | 767 | tornado.autoreload.add_reload_hook(exit) 768 | 769 | self._callbackExit = exit 770 | 771 | try: 772 | app.wait() # block 773 | except KeyboardInterrupt: 774 | print("-Process stopped") 775 | 776 | ws.exit() 777 | ws.join() 778 | return self.RETOUR 779 | 780 | 781 | def runCef(self,log=False,autoreload=False,one=False): 782 | """ Run the guy's app in a windowed cefpython3 (one client)""" 783 | self._log=log 784 | if log: 785 | handler.setLevel(logging.DEBUG) 786 | logger.setLevel(logging.DEBUG) 787 | 788 | lockPort=None 789 | if one: 790 | lp=LockPortFile(self._name) 791 | if lp.bringToFront(): 792 | return 793 | else: 794 | lockPort = lp.create() 795 | 796 | ws=WebServer( self, autoreload=autoreload ) 797 | ws.start() 798 | 799 | self.RETOUR=None 800 | try: 801 | app=CefApp(ws.startPage,self.size,lockPort=lockPort) 802 | 803 | def cefexit(v=None): 804 | self.RETOUR=v 805 | app.exit() 806 | 807 | tornado.autoreload.add_reload_hook(app.exit) 808 | 809 | self._callbackExit = cefexit 810 | 811 | try: 812 | app.wait() # block 813 | except KeyboardInterrupt: 814 | print("-Process stopped") 815 | except Exception as e: 816 | print("Trouble with CEF:",e) 817 | ws.exit() 818 | ws.join() 819 | return self.RETOUR 820 | 821 | 822 | def serve(self,port=8000,log=False,open=True,autoreload=False): 823 | """ Run the guy's app for multiple clients (web/server mode) """ 824 | self._log=log 825 | if log: 826 | handler.setLevel(logging.DEBUG) 827 | logger.setLevel(logging.DEBUG) 828 | 829 | ws=WebServer( self ,"0.0.0.0",port=port, autoreload=autoreload ) 830 | ws.start() 831 | 832 | self.RETOUR=None 833 | def exit(v=None): 834 | self.RETOUR=v 835 | ws.exit() 836 | 837 | self._callbackExit = exit 838 | print("Running", ws.startPage ) 839 | 840 | if open: #auto open browser 841 | try: 842 | import webbrowser 843 | webbrowser.open_new_tab(ws.startPage) 844 | except: 845 | pass 846 | 847 | try: 848 | ws.join() #important ! 849 | except KeyboardInterrupt: 850 | print("-Process stopped") 851 | ws.exit() 852 | return self.RETOUR 853 | 854 | 855 | def _renderJs(self,id): 856 | if self.size and self.size is not FULLSCREEN: 857 | size=self.size 858 | else: 859 | size=None 860 | routes=[k for k,v in self._routes.items() if not v.__func__.__qualname__.startswith("Guy.")] 861 | 862 | logger.debug("ROUTES: %s",routes) 863 | js = """ 864 | document.addEventListener("DOMContentLoaded", function(event) { 865 | %s 866 | },true) 867 | 868 | 869 | function setupWS( cbCnx ) { 870 | var url=%s+"/%s-ws" 871 | var ws=new WebSocket( url ); 872 | 873 | ws.onmessage = function(evt) { 874 | var r = guy._jsonParse(evt.data); 875 | guy.log("** WS RECEPT:",r) 876 | if(r.uuid) // that's a response from call py ! 877 | document.dispatchEvent( new CustomEvent('guy-'+r.uuid,{ detail: r} ) ); 878 | else if(r.jsmethod) { // call from py : self.js.<methodjs>() 879 | 880 | function sendBackReturn( response ) { 881 | var cmd={ 882 | command: "return", 883 | args: response, 884 | uuid: r.key, 885 | }; 886 | ws.send( JSON.stringify(cmd) ); 887 | guy.log("call jsmethod from py:",r.jsmethod,r.args,"-->",cmd.args) 888 | } 889 | 890 | let jsmethod=window[r.jsmethod]; 891 | if(!jsmethod) 892 | sendBackReturn( {error:"Unknown JS method "+r.jsmethod} ) 893 | else { 894 | if(jsmethod.constructor.name == 'AsyncFunction') { 895 | jsmethod.apply(window,r.args).then( function(x) { 896 | sendBackReturn( { value: x } ); 897 | }).catch(function(e) { 898 | sendBackReturn( { error: `JS Exception calling '${r.jsmethod}(...)' : ${e}` } ); 899 | }) 900 | } 901 | else { 902 | try { 903 | sendBackReturn( { value: jsmethod.apply(window,r.args) } ); 904 | } 905 | catch(e) { 906 | sendBackReturn( { error: `JS Exception calling '${r.jsmethod}(...)' : ${e}` } ); 907 | } 908 | 909 | } 910 | } 911 | } 912 | else if(r.event){ // that's an event from anywhere ! 913 | document.dispatchEvent( new CustomEvent(r.event,{ detail: r.args } ) ); 914 | } 915 | }; 916 | 917 | ws.onclose = function(evt) { 918 | guy.log("** WS Disconnected"); 919 | setTimeout( function() {setupWS(cbCnx)}, 500); 920 | }; 921 | ws.onerror = function(evt) { 922 | guy.log("** WS Disconnected"); 923 | setTimeout( function() {setupWS(cbCnx)}, 500); 924 | }; 925 | ws.onopen=function(evt) { 926 | guy.log("** WS Connected") 927 | cbCnx(ws); 928 | } 929 | 930 | return ws; 931 | } 932 | 933 | var guy={ 934 | _jsonParse: function(x) { 935 | function reviver(key, value) { 936 | const dateFormat = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$/; 937 | if (typeof value === "string" && dateFormat.test(value)) 938 | return new Date(value); 939 | else 940 | return value; 941 | } 942 | return JSON.parse(x, reviver ) 943 | }, 944 | _log: %s, 945 | log:function(_) { 946 | if(guy._log) { 947 | var args=Array.prototype.slice.call(arguments) 948 | args.unshift("--") 949 | 950 | console.log.apply(console.log,args.map( function(x) {return x==null?"NULL":x})); 951 | } 952 | }, 953 | _ws: setupWS( function(ws){guy._ws = ws; document.dispatchEvent( new CustomEvent("init") )} ), 954 | on: function( evt, callback ) { // to register an event on a callback 955 | guy.log("guy.on:","DECLARE",evt,callback.name) 956 | var listener=function(e) { callback.apply(callback,e.detail) }; 957 | document.addEventListener(evt,listener) 958 | return function() { document.removeEventListener(evt, listener) } 959 | }, 960 | 961 | emitMe: function( _) { // to emit to itself 962 | let ll=Array.prototype.slice.call(arguments) 963 | let evt=ll.shift() 964 | guy.log("guy.emitMe:", evt,ll) 965 | document.dispatchEvent( new CustomEvent(evt,{ detail: ll }) ); 966 | }, 967 | 968 | emit: function( _ ) { // to emit a event to all clients 969 | var args=Array.prototype.slice.call(arguments) 970 | guy.log("guy.emit:", args) 971 | return guy._call("emit", args) 972 | }, 973 | init: function( callback ) { 974 | function start() { 975 | guy.log("guy.init:",callback.name) 976 | document.removeEventListener("init", start) 977 | callback() 978 | } 979 | if(guy._ws.readyState == guy._ws.OPEN) 980 | start() 981 | else 982 | document.addEventListener("init", start) 983 | }, 984 | _cptFetch: 0, 985 | _applyClass: function(i) { 986 | guy._cptFetch+=i; 987 | if(guy._cptFetch>0) 988 | document.body.classList.add("wsguy") 989 | else 990 | document.body.classList.remove("wsguy") 991 | }, 992 | _call: function( method, args ) { 993 | guy._applyClass(1); 994 | guy.log("guy.call:","CALL",method,args) 995 | var cmd={ 996 | command: method, 997 | args: args, 998 | uuid: method+"-"+Math.random().toString(36).substring(2), // stamp the exchange, so the callback can be called back (thru customevent), 999 | }; 1000 | 1001 | if(guy._ws) { 1002 | guy._ws.send( JSON.stringify(cmd) ); 1003 | 1004 | return new Promise( function (resolve, reject) { 1005 | document.addEventListener('guy-'+cmd.uuid, function handler(x) { 1006 | guy._applyClass(-1); 1007 | guy.log("guy.call:","RESPONSE",method,"-->",x.detail) 1008 | this.removeEventListener('guy-'+cmd.uuid, handler); 1009 | var x=x.detail; 1010 | if(x && x.result!==undefined) { 1011 | if(x.script) 1012 | resolve( eval(x.script) ) 1013 | else 1014 | resolve(x.result) 1015 | } 1016 | else if(x && x.error!==undefined) 1017 | reject(x.error) 1018 | }); 1019 | }) 1020 | } 1021 | else 1022 | return new Promise( function (resolve, reject) { 1023 | reject("not connected"); 1024 | }) 1025 | }, 1026 | fetch: function(url,obj) { 1027 | guy.log("guy.fetch:", url, "body:",obj) 1028 | 1029 | var h={"cache-control": "no-cache"}; // !!! 1030 | if(obj && obj.headers) 1031 | Object.keys(obj.headers).forEach( function(k) { 1032 | h["set-"+k]=obj.headers[k]; 1033 | }) 1034 | var newObj = Object.assign({}, obj) 1035 | newObj.headers=h; 1036 | newObj.credentials= 'same-origin'; 1037 | return fetch( "/_/"+url,newObj ) 1038 | }, 1039 | cfg: new Proxy({}, { 1040 | get: function (obj, prop) { 1041 | return guy._call("cfg_get",[prop]) 1042 | }, 1043 | set: function (obj, prop, value) { 1044 | return guy._call("cfg_set",[prop,value]); 1045 | }, 1046 | }), 1047 | exit: function(x) {guy._call("exit",[x])}, 1048 | }; 1049 | 1050 | 1051 | var self= { 1052 | exit:function(x) {guy.exit(x)}, 1053 | %s 1054 | }; 1055 | 1056 | 1057 | 1058 | """ % ( 1059 | 'if(!document.title) document.title="%s";' % self._name, 1060 | 'window.location.origin.replace("http","ws")' if WSGUY is None else '"%s"'%WSGUY, 1061 | id, # for the socket 1062 | "true" if self._log else "false", 1063 | "\n".join(["""\n%s:function(_) {return guy._call("%s", Array.prototype.slice.call(arguments) )},""" % (k, k) for k in routes]) 1064 | ) 1065 | 1066 | return js 1067 | 1068 | def _renderHtml(self,includeGuyJs=True): 1069 | cid=self._id 1070 | 1071 | path=self._folder 1072 | html=self.__doc__ 1073 | 1074 | def rep(x): 1075 | d=self.__dict__ 1076 | d.update(self.__class__.__dict__) 1077 | for rep in re.findall("<<[^><]+>>", x): 1078 | var = rep[2:-2] 1079 | if var in d: 1080 | o=d[var] 1081 | if type(o)==str: 1082 | x=x.replace(rep, o) 1083 | else: 1084 | x=x.replace(rep, jDumps( o )) 1085 | return x 1086 | 1087 | def repgjs(x): 1088 | return re.sub('''src *= *(?P<quote>["'])[^(?P=quote)]*guy\\.js[^(?P=quote)]*(?P=quote)''','src="%s-js"'%(cid,),x) 1089 | 1090 | 1091 | def _caller(self,method:str,args=[]): 1092 | isBound=hasattr(method, '__self__') 1093 | if isBound: 1094 | r=method(*args) 1095 | else: 1096 | r=method(self, *args) 1097 | return r 1098 | 1099 | if hasattr(self,"render"): 1100 | html = _caller(self, self.render, [path] ) 1101 | html=repgjs(html) 1102 | return rep(html) 1103 | else: 1104 | if hasattr(self,"_render"): 1105 | print("**DEPRECATING** use of _render() ... use render() instead !") 1106 | html = _caller(self, self.render, [path] ) 1107 | html=repgjs(html) 1108 | return rep(html) 1109 | else: 1110 | if html: 1111 | if includeGuyJs: html=("""<script src="guy.js"></script>""")+ html 1112 | html=repgjs(html) 1113 | return rep(html) 1114 | else: 1115 | f=os.path.join(path,FOLDERSTATIC,"%s.html" % self._name) 1116 | if os.path.isfile(f): 1117 | html=readTextFile(f) 1118 | html=repgjs(html) 1119 | return rep(html) 1120 | else: 1121 | return "ERROR: can't find '%s'" % f 1122 | 1123 | class Guy(GuyBase): 1124 | _wsock=None # when cloned and connected to a client/wsock (only the cloned instance set this) 1125 | _instances={} # class variable handling all rendered instances 1126 | 1127 | size=None 1128 | def __init__(self): 1129 | self.parent=None 1130 | self._log=False 1131 | self._name = self.__class__.__name__ 1132 | self._id=self._name+"_"+hex(id(self))[2:] # unique (readable) id to this instance 1133 | self._callbackExit=None #public callback when "exit" 1134 | if hasattr(sys, "_MEIPASS"): # when freezed with pyinstaller ;-) 1135 | self._folder=sys._MEIPASS 1136 | else: 1137 | self._folder = os.path.dirname( inspect.getfile( self.__class__ ) ) # *ME* 1138 | 1139 | self._routes={} 1140 | for n, v in inspect.getmembers(self, inspect.ismethod): 1141 | if not v.__func__.__qualname__.startswith("GuyBase."): # only "Guy." and its subclass 1142 | if not n.startswith("_") and n!="render" : 1143 | #~ print("------------Route %s: %s" %(self._id,n)) 1144 | self._routes[n]=v 1145 | 1146 | Guy._instances[self._id]=self # When render -> save the instance in the pool 1147 | 1148 | 1149 | @property 1150 | def cfg(self): 1151 | class Proxy: 1152 | def __init__(sself): 1153 | if ISANDROID: 1154 | exepath=os.path.abspath(os.path.realpath(sys.argv[0])) 1155 | path=os.path.join( os.path.dirname(exepath), "..", "config.json" ) 1156 | else: 1157 | exepath=os.path.abspath(os.path.realpath(sys.argv[0])) # or os.path.abspath(__main__.__file__) 1158 | classpath= os.path.abspath( os.path.realpath(inspect.getfile( self.__class__ )) ) 1159 | if not exepath.endswith(".exe") and classpath!=exepath: # as module 1160 | path=os.path.join( os.path.expanduser("~") , ".%s.json"%os.path.basename(exepath) ) 1161 | else: # as exe 1162 | path = os.path.join( os.path.dirname(exepath), "config.json" ) 1163 | 1164 | logger.debug("Use config: %s",path) 1165 | sself.__o=JDict( path ) 1166 | sself._file=path # new >0.5.3 1167 | def __setattr__(self,k,v): 1168 | if k.startswith("_"): 1169 | super(Proxy, self).__setattr__(k, v) 1170 | else: 1171 | self.__o.set(k,v) 1172 | def __getattr__(self,k): 1173 | if k.startswith("_"): 1174 | return super(Proxy, self).__getattr__(k) 1175 | else: 1176 | return self.__o.get(k) 1177 | return Proxy() 1178 | 1179 | def cfg_set(self, key, value): setattr(self.cfg,key,value) 1180 | def cfg_get(self, key=None): return getattr(self.cfg,key) 1181 | 1182 | 1183 | @property 1184 | def js(self): 1185 | class Proxy: 1186 | def __getattr__(sself,jsmethod): 1187 | async def _(*args): 1188 | return await self._callMe(jsmethod,*args) 1189 | return _ 1190 | return Proxy() 1191 | 1192 | def exit(self,v=None): 1193 | if self._callbackExit: 1194 | self._callbackExit(v) 1195 | else: 1196 | self.parent._callbackExit(v) 1197 | 1198 | async def emit(self, event, *args): 1199 | await emit(event, *args) 1200 | 1201 | async def emitMe(self,event, *args): 1202 | logger.debug(">>> emitMe %s (%s)",event,args) 1203 | await sockwrite(self._wsock,event=event,args=args) 1204 | 1205 | async def _callMe(self,jsmethod, *args): 1206 | logger.debug(">>> callMe %s (%s)",jsmethod,args) 1207 | key=uuid.uuid4().hex 1208 | # send jsmethod 1209 | await sockwrite(self._wsock,jsmethod=jsmethod,args=args,key=key) 1210 | # wait the return (of the key) 1211 | while 1: 1212 | if key in WebSocketHandler.returns: 1213 | response=WebSocketHandler.returns[key] 1214 | del WebSocketHandler.returns[key] 1215 | if "error" in response: 1216 | raise JSException(response["error"]) 1217 | else: 1218 | return response.get("value") 1219 | await asyncio.sleep(0.01) 1220 | 1221 | 1222 | def _getRoutage(self,method): # or None 1223 | return self._routes.get(method) 1224 | 1225 | #~ def __call__(self,theSock,method,*args): 1226 | #~ #################################################################### 1227 | #~ ## not the best (no concurrent client in servermode) 1228 | #~ #################################################################### 1229 | 1230 | #~ self._wsock=theSock 1231 | 1232 | #~ for k, v in self._routes.items(): 1233 | #~ setattr(self,k,v) #rebound ! (for init()) 1234 | 1235 | #~ function = self._getRoutage(method) 1236 | #~ print("__CALL__",method,args) 1237 | #~ return function(*args) 1238 | 1239 | 1240 | def __call__(self,theSock,method,*args): 1241 | #################################################################### 1242 | ## create a context, contextual to the socket "theSock" -> context 1243 | #################################################################### 1244 | context = copy.copy(self) # important (not deepcopy!), to be able to share mutable 1245 | 1246 | for n, v in inspect.getmembers(context): 1247 | if n in self._routes.keys(): 1248 | if inspect.isfunction(v): 1249 | v=types.MethodType( v, context ) 1250 | setattr( context, n, v ) 1251 | context._routes[n]=v 1252 | 1253 | context._wsock=theSock 1254 | #################################################################### 1255 | try: 1256 | function = context._getRoutage(method) 1257 | r=function(*args) 1258 | finally: 1259 | del context 1260 | return r 1261 | 1262 | 1263 | 1264 | 1265 | def runAndroid(ga): 1266 | import kivy 1267 | from kivy.app import App 1268 | from kivy.utils import platform 1269 | from kivy.uix.widget import Widget 1270 | from kivy.clock import Clock 1271 | from kivy.logger import Logger 1272 | 1273 | def run_on_ui_thread(arg): 1274 | pass 1275 | 1276 | webView = None 1277 | webViewClient = None 1278 | #~ webChromeClient = None 1279 | activity = None 1280 | if platform == 'android': 1281 | from jnius import autoclass 1282 | from android.runnable import run_on_ui_thread 1283 | webView = autoclass('android.webkit.WebView') 1284 | webViewClient = autoclass('android.webkit.WebViewClient') 1285 | #~ webChromeClient = autoclass('android.webkit.WebChromeClient') 1286 | activity = autoclass('org.kivy.android.PythonActivity').mActivity 1287 | 1288 | 1289 | 1290 | class Wv(Widget): 1291 | def __init__(self, guyWindow ): 1292 | self.f2 = self.create_webview # important 1293 | super(Wv, self).__init__() 1294 | self.visible = False 1295 | 1296 | def exit(v): 1297 | activity.finish() 1298 | App.get_running_app().stop() 1299 | os._exit(0) 1300 | 1301 | guyWindow._callbackExit = exit 1302 | 1303 | self.ws=WebServer( guyWindow ) 1304 | self.ws.start() 1305 | 1306 | Clock.schedule_once(self.create_webview, 0) 1307 | 1308 | @run_on_ui_thread 1309 | def create_webview(self, *args): 1310 | webview = webView(activity) 1311 | webview.getSettings().setJavaScriptEnabled(True) 1312 | webview.getSettings().setDomStorageEnabled(True) 1313 | webview.setWebViewClient(webViewClient()) 1314 | #~ webview.setWebChromeClient(webChromeClient()) 1315 | activity.setContentView(webview) 1316 | webview.loadUrl(self.ws.startPage) 1317 | 1318 | class ServiceApp(App): 1319 | def build(self): 1320 | return Wv( ga ) 1321 | 1322 | ServiceApp().run() 1323 | 1324 | 1325 | 1326 | if __name__ == "__main__": 1327 | #~ from testTordu import Tordu as GuyApp 1328 | # from testPrompt import Win as GuyApp 1329 | # GuyApp().run() 1330 | pass 1331 | --------------------------------------------------------------------------------