├── 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 |
2 |
3 | {{cpt}}
4 | ++
5 |
6 |
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 | emit
17 | emit glob
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 | """test """
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 |
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 |
7 |
8 | <> ?
9 |
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 <>
11 |
12 | Test
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 | Via HTTP hook
27 | classic redirection
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 |
15 |
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 |
11 |
12 |
18 |
19 |
20 | Open a second tab (better: from another computer)
21 | to see this simple tchat on ;-)
22 | """
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 |
21 |
22 | set
23 |
24 |
25 | If you try to run a second one
26 | It will focus to this one !
27 | (and keep storage !)
28 |
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 |
11 | X
12 |
18 | Tap Tempo!
19 |
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 |
11 |
21 |
22 |
23 | test
24 | X
25 |
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 "]
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 |
8 |
9 |
14 |
15 | sync quick
16 | sync long (block ui)
17 | async long
18 |
19 |
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 |
13 |
18 |
19 |
minipy
20 |
24 | exit
25 |
26 |
29 |
"""
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 |
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 |
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 |
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 |
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/.html
8 | size=guy.FULLSCREEN
9 | __doc__="""
10 |
18 |
37 |
38 |
39 |
40 | X
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 |
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__=""""""
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 |
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 |
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 |
16 |
17 |
18 |
19 | On Ubuntu
20 |
21 |
22 | On Android10
23 |
24 |
25 |
26 |
27 |
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 |
14 |
15 |
16 |
17 |
18 | drop files here
19 |
20 |
21 |
22 |
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 |
19 |
20 | t1
21 | Test
22 |
23 |
24 | go to polo
25 | go to nowhere
26 | go to logo.png
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 |
45 |
46 | t2
47 | Test
48 |
49 | go to marco
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/.html
6 | __doc__="""
7 |
19 |
20 |
21 | Go
22 |
23 |
24 | Go
25 |
26 |
27 |
39 |
40 |
41 |
42 | Run in a second tab, to see
43 | that it's isolated by instance !
44 |
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 |
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 |
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 |
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 |
53 | 1
54 | """
55 |
56 | class W2(Guy):
57 | __doc__="""
58 |
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 | <>
24 | go to Page1
25 | go to Page1 too !
26 | """
27 | def __init__(self,txt):
28 | Guy.__init__(self)
29 | self.txt = txt
30 |
31 | class Page1(Guy):
32 | """
33 | go to Page2
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 "]
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 | [](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 | """test """
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 |
45 |
46 |
47 |
48 | On Ubuntu
49 |
50 |
51 | On Android10
52 |
53 |
54 |
55 |
56 |
57 |
58 | [](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 |
36 | call js ok
37 | call async js ok
38 | call async long js ok
39 | call js not found
40 | call js ko
41 | call async js ko
42 | test promt()
43 |
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 |
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 ``~/..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 | """test """
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 |
30 |
31 |
Wait...
32 |
33 | """
34 |
35 |
36 | class MsgBox(Guy):
37 | size=(300,150)
38 | __doc__="""
39 |
42 |
43 |
<>
44 | OK
45 |
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 |
59 |
60 |
<>
61 | OK
62 | Cancel
63 |
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 |
82 |
83 |
<>
84 |
88 | Cancel
89 |
90 |
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 |
126 |
157 |
158 | X
159 | My GuyAPP ;-)
160 |
161 |
162 | Name: <>
163 |
164 |
165 | Surname: <>
166 |
167 |
168 | test
169 | Wait
170 |
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 |
81 | replay
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 |
63 |
64 |
65 |
66 | Clear
67 | Random
68 | Resolv
69 |
70 |
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 `~/..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?" 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__="""test """
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/<>/templates/AndroidManifest.tmpl.xml`
100 | (.buildozer/android/platform/build/dists/com.guy/templates/AndroidManifest.tmpl.xml)
101 |
102 | Add `android:usesCleartextTraffic="true"` in tag `` in `AndroidManifest.tmpl.xml`
103 |
104 | Search the tag `` which look like this :
105 |
106 | ```xml
107 |
112 | ```
113 | And change it to :
114 | ```xml hl_lines="3"
115 |
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/= 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__="""test """
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__="""test """
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 ! (`<>` 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 `` in your html.
52 |
53 | **TODO** : talk about template engine ! (`<>` 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("",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 `` 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 ``~/..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 | """test """
170 | size=(400,400)
171 | ```
172 |
173 | ```python
174 | class Simple(guy.Guy):
175 | """test """
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 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: ()"
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\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"\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>> 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.+)', ProxyHandler,dict(instance=self.instance)),
454 | (r'/(?P[^/]+)-ws', WebSocketHandler,dict(instance=self.instance)),
455 | (r'/(?P[^/]+)-js', GuyJSHandler,dict(instance=self.instance)),
456 | (r'/(?P[^\.]*)', 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.()
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["'])[^(?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=("""""")+ 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 |
--------------------------------------------------------------------------------