├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README-old.md
├── README.md
├── async_eel
├── __init__.py
├── __main__.py
├── browsers.py
├── chrome.py
├── edge.py
├── eel.js
├── electron.py
└── utils.py
├── examples
├── 01 - hello_world-Edge
│ └── hello.py
├── 01 - hello_world
│ ├── hello.py
│ └── web
│ │ ├── favicon.ico
│ │ └── hello.html
├── 02 - callbacks
│ ├── callbacks.py
│ └── web
│ │ ├── callbacks.html
│ │ └── favicon.ico
├── 03 - sync_callbacks
│ ├── sync_callbacks.py
│ └── web
│ │ ├── favicon.ico
│ │ └── sync_callbacks.html
├── 04 - file_access
│ ├── Screenshot.png
│ ├── file_access.py
│ └── web
│ │ ├── favicon.ico
│ │ └── file_access.html
├── 05 - input
│ ├── script.py
│ └── web
│ │ ├── favicon.ico
│ │ └── main.html
├── 06 - jinja_templates
│ ├── hello.py
│ └── web
│ │ ├── favicon.ico
│ │ └── templates
│ │ ├── base.html
│ │ ├── hello.html
│ │ └── page2.html
├── 07 - CreateReactApp
│ ├── .gitignore
│ ├── Demo.png
│ ├── README.md
│ ├── eel_CRA.py
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ ├── src
│ │ ├── App.css
│ │ ├── App.test.tsx
│ │ ├── App.tsx
│ │ ├── index.css
│ │ ├── index.tsx
│ │ ├── logo.svg
│ │ ├── react-app-env.d.ts
│ │ └── serviceWorker.ts
│ └── tsconfig.json
├── 08 - Eelectron-quick-start
│ ├── .gitignore
│ ├── hello.py
│ ├── main.js
│ ├── package-lock.json
│ ├── package.json
│ └── web
│ │ ├── favicon.ico
│ │ └── hello.html
└── 08 - disable_cache
│ ├── disable_cache.py
│ └── web
│ ├── disable_cache.html
│ ├── dont_cache_me.js
│ └── favicon.ico
├── requirements.txt
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | dist
3 | build
4 | Eel.egg-info
5 | .tmp
6 | .DS_Store
7 | *.pyc
8 | *.swp
9 | venv/
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | cache: pip
3 | python:
4 | - 2.7
5 | - 3.6
6 | matrix:
7 | allow_failures:
8 | - python: 2.7
9 | install:
10 | #- pip install -r requirements.txt
11 | - pip install flake8 # pytest # add another testing frameworks later
12 | before_script:
13 | # stop the build if there are Python syntax errors or undefined names
14 | - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
15 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
16 | - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
17 | script:
18 | - true # pytest --capture=sys # add other tests here
19 | notifications:
20 | on_success: change
21 | on_failure: change # `always` will be the setting once code changes slow down
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ### v0.10.4
4 | * Fix PyPi project description.
5 |
6 | ### v0.10.3
7 | * Fix a bug that prevented using Eel without Jinja templating.
8 |
9 | ### v0.10.2
10 | * Only render templates from within the declared jinja template directory.
11 |
12 | ### v0.10.1
13 | * Avoid name collisions when using Electron, so jQuery etc work normally
14 |
15 | ## v0.10.0
16 | * Corrective version bump after new feature included in 0.9.13
17 | * Fix a bug with example 06 for Jinja templating; the `templates` kwarg to `eel.start` takes a filepath, not a bool.
18 |
19 | ### v0.9.13
20 | * Add support for Jinja templating.
21 |
22 | ### Earlier
23 | * No changelog notes for earlier versions.
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Chris Knott
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
--------------------------------------------------------------------------------
/README-old.md:
--------------------------------------------------------------------------------
1 | # Eel
2 |
3 | Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, with full access to Python capabilities and libraries.
4 |
5 | > **Eel hosts a local webserver, then lets you annotate functions in Python so that they can be called from Javascript, and vice versa.**
6 |
7 | Eel is designed to take the hassle out of writing short and simple GUI applications. If you are familiar with Python and web development, probably just jump to [this example](https://github.com/ChrisKnott/Eel/tree/master/examples/04%20-%20file_access) which picks random file names out of the given folder (something that is impossible from a browser).
8 |
9 |
10 |
11 |
12 |
13 | - [Eel](#eel)
14 | - [Intro](#intro)
15 | - [Install](#install)
16 | - [Usage](#usage)
17 | - [Directory Structure](#directory-structure)
18 | - [Starting the app](#starting-the-app)
19 | - [App options](#app-options)
20 | - [Chrome/Chromium flags](#chromechromium-flags)
21 | - [Exposing functions](#exposing-functions)
22 | - [Eello, World!](#eello-world)
23 | - [Return values](#return-values)
24 | - [Callbacks](#callbacks)
25 | - [Synchronous returns](#synchronous-returns)
26 | - [Asynchronous Python](#asynchronous-python)
27 | - [Building distributable binary with PyInstaller](#building-distributable-binary-with-pyinstaller)
28 | - [Microsoft Edge](#microsoft-edge)
29 |
30 |
31 |
32 | ## Intro
33 |
34 | There are several options for making GUI apps in Python, but if you want to use HTML/JS (in order to use jQueryUI or Bootstrap, for example) then you generally have to write a lot of boilerplate code to communicate from the Client (Javascript) side to the Server (Python) side.
35 |
36 | The closest Python equivalent to Electron (to my knowledge) is [cefpython](https://github.com/cztomczak/cefpython). It is a bit heavy weight for what I wanted.
37 |
38 | Eel is not as fully-fledged as Electron or cefpython - it is probably not suitable for making full blown applications like Atom - but it is very suitable for making the GUI equivalent of little utility scripts that you use internally in your team.
39 |
40 | For some reason many of the best-in-class number crunching and maths libraries are in Python (Tensorflow, Numpy, Scipy etc) but many of the best visualization libraries are in Javascript (D3, THREE.js etc). Hopefully Eel makes it easy to combine these into simple utility apps for assisting your development.
41 |
42 | ## Install
43 |
44 | Install from pypi with `pip`:
45 |
46 | ```shell
47 | pip install async-eel
48 | ```
49 |
50 | ## Usage
51 |
52 | ### Directory Structure
53 |
54 | An Eel application will be split into a frontend consisting of various web-technology files (.html, .js, .css) and a backend consisting of various Python scripts.
55 |
56 | All the frontend files should be put in a single directory (they can be further divided into folders inside this if necessary).
57 |
58 | ```
59 | my_python_script.py <-- Python scripts
60 | other_python_module.py
61 | static_web_folder/ <-- Web folder
62 | main_page.html
63 | css/
64 | style.css
65 | img/
66 | logo.png
67 | ```
68 |
69 | ### Starting the app
70 |
71 | Suppose you put all the frontend files in a directory called `web`, including your start page `main.html`, then the app is started like this;
72 |
73 | ```python
74 | import eel
75 | eel.init('web')
76 | eel.start('main.html')
77 | ```
78 |
79 | This will start a webserver on the default settings (http://localhost:8000) and open a browser to http://localhost:8000/main.html.
80 |
81 | If Chrome or Chromium is installed then by default it will open in that in App Mode (with the `--app` cmdline flag), regardless of what the OS's default browser is set to (it is possible to override this behaviour).
82 |
83 | ### App options
84 |
85 | Additional options can be passed to `eel.start()` as keyword arguments.
86 |
87 | Some of the options include the mode the app is in (e.g. 'chrome'), the port the app runs on, the host name of the app, and adding additional command line flags.
88 |
89 | As of Eel 1.0.0, the following options are available to `start()`:
90 | - **mode**, a string specifying what browser to use (e.g. `'chrome'`, `'electron'`, `'edge'`, `'custom'`). Can also be `None` or `False` to not open a window. *Default: `'chrome'`*
91 | - **host**, a string specifying what hostname to use for the Bottle server. *Default: `'localhost'`)*
92 | - **port**, an int specifying what port to use for the Bottle server. Use `0` for port to be picked automatically. *Default: `8000`*.
93 | - **block**, a bool saying whether or not the call to `start()` should block the calling thread. *Default: `True`*
94 | - **jinja_templates**, a string specifying a folder to use for Jinja2 templates, e.g. `my_templates`. *Default: `None`*
95 | - **cmdline_args**, a list of strings to pass to the command to start the browser. For example, we might add extra flags for Chrome; ```eel.start('main.html', mode='chrome-app', port=8080, cmdline_args=['--start-fullscreen', '--browser-startup-dialog'])```. *Default: `[]`*
96 | - **size**, a tuple of ints specifying the (width, height) of the main window in pixels *Default: `None`*
97 | - **position**, a tuple of ints specifying the (left, top) of the main window in pixels *Default: `None`*
98 | - **geometry**, a dictionary specifying the size and position for all windows. The keys should be the relative path of the page, and the values should be a dictionary of the form `{'size': (200, 100), 'position': (300, 50)}`. *Default: {}*
99 | - **close_callback**, a lambda or function that is called when a websocket to a window closes (i.e. when the user closes the window). It should take two arguments; a string which is the relative path of the page that just closed, and a list of other websockets that are still open. *Default: `None`*
100 |
101 |
102 |
103 | ### Exposing functions
104 |
105 | In addition to the files in the frontend folder, a Javascript library will be served at `/eel.js`. You should include this in any pages:
106 |
107 | ```html
108 |
109 | ```
110 |
111 | Including this library creates an `eel` object which can be used to communicate with the Python side.
112 |
113 | Any functions in the Python code which are decorated with `@eel.expose` like this...
114 |
115 | ```python
116 | @eel.expose
117 | def my_python_function(a, b):
118 | print(a, b, a + b)
119 | ```
120 |
121 | ...will appear as methods on the `eel` object on the Javascript side, like this...
122 |
123 | ```javascript
124 | console.log("Calling Python...");
125 | eel.my_python_function(1, 2); // This calls the Python function that was decorated
126 | ```
127 |
128 | Similarly, any Javascript functions which are exposed like this...
129 |
130 | ```javascript
131 | eel.expose(my_javascript_function);
132 | function my_javascript_function(a, b, c, d) {
133 | if (a < b) {
134 | console.log(c * d);
135 | }
136 | }
137 | ```
138 |
139 | can be called from the Python side like this...
140 |
141 | ```python
142 | print('Calling Javascript...')
143 | eel.my_javascript_function(1, 2, 3, 4) # This calls the Javascript function
144 | ```
145 |
146 | When passing complex objects as arguments, bear in mind that internally they are converted to JSON and sent down a websocket (a process that potentially loses information).
147 |
148 | ### Eello, World!
149 |
150 | > See full example in: [examples/01 - hello_world](https://github.com/ChrisKnott/Eel/tree/master/examples/01%20-%20hello_world)
151 |
152 | Putting this together into a **Hello, World!** example, we have a short HTML page, `web/hello.html`:
153 |
154 | ```html
155 |
156 |
157 |
158 | Hello, World!
159 |
160 |
161 |
162 |
171 |
172 |
173 |
174 | Hello, World!
175 |
176 |
177 | ```
178 |
179 | and a short Python script `hello.py`:
180 |
181 | ```python
182 | import eel
183 |
184 | # Set web files folder and optionally specify which file types to check for eel.expose()
185 | # *Default allowed_extensions are: ['.js', '.html', '.txt', '.htm', '.xhtml']
186 | eel.init('web', allowed_extensions=['.js', '.html'])
187 |
188 | @eel.expose # Expose this function to Javascript
189 | def say_hello_py(x):
190 | print('Hello from %s' % x)
191 |
192 | say_hello_py('Python World!')
193 | eel.say_hello_js('Python World!') # Call a Javascript function
194 |
195 | eel.start('hello.html') # Start (this blocks and enters loop)
196 | ```
197 |
198 | If we run the Python script (`python hello.py`), then a browser window will open displaying `hello.html`, and we will see...
199 |
200 | ```
201 | Hello from Python World!
202 | Hello from Javascript World!
203 | ```
204 |
205 | ...in the terminal, and...
206 |
207 | ```
208 | Hello from Javascript World!
209 | Hello from Python World!
210 | ```
211 |
212 | ...in the browser console (press F12 to open).
213 |
214 | You will notice that in the Python code, the Javascript function is called before the browser window is even started - any early calls like this are queued up and then sent once the websocket has been established.
215 |
216 | ### Return values
217 |
218 | While we want to think of our code as comprising a single application, the Python interpreter and the browser window run in separate processes. This can make communicating back and forth between them a bit of a mess, especially if we always had to explicitly _send_ values from one side to the other.
219 |
220 | Eel supports two ways of retrieving _return values_ from the other side of the app, which helps keep the code concise.
221 |
222 | #### Callbacks
223 |
224 | When you call an exposed function, you can immediately pass a callback function afterwards. This callback will automatically be called asynchrounously with the return value when the function has finished executing on the other side.
225 |
226 | For example, if we have the following function defined and exposed in Javascript:
227 |
228 | ```javascript
229 | eel.expose(js_random);
230 | function js_random() {
231 | return Math.random();
232 | }
233 | ```
234 |
235 | Then in Python we can retrieve random values from the Javascript side like so:
236 |
237 | ```python
238 | def print_num(n):
239 | print('Got this from Javascript:', n)
240 |
241 | # Call Javascript function, and pass explicit callback function
242 | eel.js_random()(print_num)
243 |
244 | # Do the same with an inline lambda as callback
245 | eel.js_random()(lambda n: print('Got this from Javascript:', n))
246 | ```
247 |
248 | (It works exactly the same the other way around).
249 |
250 | #### Synchronous returns
251 |
252 | In most situations, the calls to the other side are to quickly retrieve some piece of data, such as the state of a widget or contents of an input field. In these cases it is more convenient to just synchronously wait a few milliseconds then continue with your code, rather than breaking the whole thing up into callbacks.
253 |
254 | To synchronously retrieve the return value, simply pass nothing to the second set of brackets. So in Python we would write:
255 |
256 | ```python
257 | n = eel.js_random()() # This immediately returns the value
258 | print('Got this from Javascript:', n)
259 | ```
260 |
261 | You can only perform synchronous returns after the browser window has started (after calling `eel.start()`), otherwise obviously the call with hang.
262 |
263 | In Javascript, the language doesn't allow us to block while we wait for a callback, except by using `await` from inside an `async` function. So the equivalent code from the Javascript side would be:
264 |
265 | ```javascript
266 | async function run() {
267 | // Inside a function marked 'async' we can use the 'await' keyword.
268 |
269 | let n = await eel.py_random()(); // Must prefix call with 'await', otherwise it's the same syntax
270 | console.log("Got this from Python: " + n);
271 | }
272 |
273 | run();
274 | ```
275 |
276 | ## Asynchronous Python
277 |
278 | Eel is built on Bottle and Gevent, which provide an asynchronous event loop similar to Javascript. A lot of Python's standard library implicitly assumes there is a single execution thread - to deal with this, Gevent can "[monkey patch](https://en.wikipedia.org/wiki/Monkey_patch)" many of the standard modules such as `time`. ~~This monkey patching is done automatically when you call `import eel`~~. If you need monkey patching you should `import gevent.monkey` and call `gevent.monkey.patch_all()` _before_ you `import eel`. Monkey patching can interfere with things like debuggers so should be avoided unless necessary.
279 |
280 | For most cases you should be fine by avoiding using `time.sleep()` and instead using the versions provided by `gevent`. For convenience, the two most commonly needed gevent methods, `sleep()` and `spawn()` are provided directly from Eel (to save importing `time` and/or `gevent` as well).
281 |
282 | In this example...
283 |
284 | ```python
285 | import eel
286 | eel.init('web')
287 |
288 | def my_other_thread():
289 | while True:
290 | print("I'm a thread")
291 | eel.sleep(1.0) # Use eel.sleep(), not time.sleep()
292 |
293 | eel.spawn(my_other_thread)
294 |
295 | eel.start('main.html', block=False) # Don't block on this call
296 |
297 | while True:
298 | print("I'm a main loop")
299 | eel.sleep(1.0) # Use eel.sleep(), not time.sleep()
300 | ```
301 |
302 | ...we would then have three "threads" (greenlets) running;
303 |
304 | 1. Eel's internal thread for serving the web folder
305 | 2. The `my_other_thread` method, repeatedly printing **"I'm a thread"**
306 | 3. The main Python thread, which would be stuck in the final `while` loop, repeatedly printing **"I'm a main loop"**
307 |
308 | ## Building distributable binary with PyInstaller
309 |
310 | If you want to package your app into a program that can be run on a computer without a Python interpreter installed, you should use **PyInstaller**.
311 |
312 | 1. Configure a virtualenv with desired Python version and minimum necessary Python packages
313 | 2. Install PyInstaller `pip install PyInstaller`
314 | 3. In your app's folder, run `python -m eel [your_main_script] [your_web_folder]` (for example, you might run `python -m eel hello.py web`)
315 | 4. This will create a new folder `dist/`
316 | 5. Valid PyInstaller flags can be passed through, such as excluding modules with the flag: `--exclude module_name`. For example, you might run `python -m eel file_access.py web --exclude win32com --exclude numpy --exclude cryptography`
317 | 6. When happy that your app is working correctly, add `--onefile --noconsole` flags to build a single executable file
318 |
319 | Consult the [documentation for PyInstaller](http://PyInstaller.readthedocs.io/en/stable/) for more options.
320 |
321 | ## Microsoft Edge
322 |
323 | For Windows 10 users, Microsoft Edge (`eel.start(.., mode='edge')`) is installed by default and a useful fallback if a preferred browser is not installed. See the examples:
324 |
325 | - A Hello World example using Microsoft Edge: [examples/01 - hello_world-Edge/](https://github.com/ChrisKnott/Eel/tree/master/examples/01%20-%20hello_world-Edge)
326 | - Example implementing browser-fallbacks: [examples/07 - CreateReactApp/eel_CRA.py](https://github.com/ChrisKnott/Eel/tree/master/examples/07%20-%20CreateReactApp/eel_CRA.py)
327 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | async-Eel
2 | ====
3 | Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps,
4 | with full access to Python capabilities and libraries.
5 |
6 | About
7 | ----
8 | * Python**3.6+**
9 | * use `asyncio`
10 | * [original README.md](README-old.md)
11 | * [original repository](https://github.com/ChrisKnott/Eel)
12 |
13 | I design this library for `asyncio` event loop users.
14 |
15 | The original uses `gevent` and both event loops have difficulty in using simultaneously.
16 |
17 | Install
18 | ----
19 | ```bash
20 | pip install async-eel
21 | ```
22 |
23 | How to use
24 | ----
25 | It looks like same usage, but something is different, please look at [example codes](examples).
26 |
27 | I forked [eel_bootstrap(JP)](https://github.com/namuyan/eel_bootstrap) as good example.
28 |
29 | Author
30 | ----
31 | [@namuyan_mine](https://twitter.com/namuyan_mine)
32 |
33 | Licence
34 | ----
35 | [MIT](LICENSE)
36 |
37 |
--------------------------------------------------------------------------------
/async_eel/__init__.py:
--------------------------------------------------------------------------------
1 | from async_eel.utils import *
2 | from async_eel import browsers
3 | from aiohttp.web_ws import WebSocketResponse
4 | from aiohttp.web import BaseRequest
5 | from aiohttp import web
6 | from expiringdict import ExpiringDict
7 | from typing import Optional, Callable, Dict
8 | from logging import getLogger, ERROR
9 | from random import random
10 | import socket
11 | import asyncio
12 | import json
13 | import re
14 | import sys
15 | import os
16 |
17 |
18 | # logging
19 | getLogger('aiohttp').setLevel(ERROR)
20 | log = getLogger(__name__)
21 |
22 | # inner objects
23 | routes = web.RouteTableDef()
24 | loop = asyncio.get_event_loop()
25 |
26 | # inner vars
27 | _eel_js_file = os.path.join(os.path.dirname(__file__), 'eel.js')
28 | _eel_js = open(_eel_js_file, encoding='utf-8').read()
29 | _websockets = list()
30 | _call_return_futures: Dict[int, asyncio.Future] = ExpiringDict(max_len=5000, max_age_seconds=300)
31 | _call_number = 0
32 | _exposed_functions = dict()
33 | _js_functions = list()
34 | _mock_queue = list()
35 | _mock_queue_done = set()
36 |
37 | # All start() options must provide a default value and explanation here
38 | _start_args = {
39 | 'mode': 'chrome', # What browser is used
40 | 'host': 'localhost', # Hostname use for Bottle server
41 | 'port': 8000, # Port used for Bottle server (use 0 for auto)
42 | 'block': True, # Whether start() blocks calling thread
43 | 'jinja_templates': None, # Folder for jinja2 templates
44 | 'cmdline_args': ['--disable-http-cache'], # Extra cmdline flags to pass to browser start
45 | 'size': None, # (width, height) of main window
46 | 'position': None, # (left, top) of main window
47 | 'geometry': {}, # Dictionary of size/position for all windows
48 | 'close_callback': None, # Callback for when all windows have closed
49 | 'app_mode': True, # (Chrome specific option)
50 | 'all_interfaces': False, # Allow bottle server to listen for connections on all interfaces
51 | 'disable_cache': True, # Sets the no-store response header when serving assets
52 | 'auto_close': True, # Auto close process when websocket connection is zero
53 | }
54 |
55 | # == Temporary (suppressable) error message to inform users of breaking API change for v1.0.0 ===
56 | _start_args['suppress_error'] = False
57 | api_error_message = '''
58 | ----------------------------------------------------------------------------------
59 | 'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel
60 | To suppress this error, add 'suppress_error=True' to start() call.
61 | This option will be removed in future versions
62 | ----------------------------------------------------------------------------------
63 | '''
64 | # ===============================================================================================
65 |
66 | # Public functions
67 |
68 |
69 | def expose(name_or_function=None):
70 | # Deal with '@eel.expose()' - treat as '@eel.expose'
71 | if name_or_function is None:
72 | return expose
73 |
74 | if isinstance(name_or_function, str): # Called as '@eel.expose("my_name")'
75 | name = name_or_function
76 |
77 | def decorator(function):
78 | _expose(name, function)
79 | return function
80 | return decorator
81 | else:
82 | function = name_or_function
83 | _expose(function.__name__, function)
84 | return function
85 |
86 |
87 | def init(path, allowed_extensions=('.js', '.html', '.txt', '.htm', '.xhtml', '.vue')):
88 | global root_path, _js_functions
89 | root_path = _get_real_path(path)
90 |
91 | js_functions = set()
92 | for root, _, files in os.walk(root_path):
93 | for name in files:
94 | if not any(name.endswith(ext) for ext in allowed_extensions):
95 | continue
96 |
97 | try:
98 | with open(os.path.join(root, name), encoding='utf-8') as file:
99 | contents = file.read()
100 | expose_calls = set()
101 | finder = re.findall(r'eel\.expose\(([^\)]+)\)', contents)
102 | for expose_call in finder:
103 | # If name specified in 2nd argument, strip quotes and store as function name
104 | if ',' in expose_call:
105 | expose_call = re.sub(r'["\']', '', expose_call.split(',')[1])
106 | expose_call = expose_call.strip()
107 | # Verify that function name is valid
108 | msg = "eel.expose() call contains '(' or '='"
109 | assert re.findall(r'[\(=]', expose_call) == [], msg
110 | expose_calls.add(expose_call)
111 | js_functions.update(expose_calls)
112 | except UnicodeDecodeError:
113 | pass # Malformed file probably
114 |
115 | _js_functions = list(js_functions)
116 | for js_function in _js_functions:
117 | _mock_js_function(js_function)
118 |
119 |
120 | async def start(*start_urls, **kwargs):
121 | try:
122 | _start_args.update(kwargs)
123 |
124 | if 'options' in kwargs:
125 | if _start_args['suppress_error']:
126 | _start_args.update(kwargs['options'])
127 | else:
128 | raise RuntimeError(api_error_message)
129 |
130 | if _start_args['port'] == 0:
131 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
132 | sock.bind(('localhost', 0))
133 | _start_args['port'] = sock.getsockname()[1]
134 | sock.close()
135 |
136 | if _start_args['jinja_templates'] is not None:
137 | from jinja2 import Environment, FileSystemLoader, select_autoescape
138 | templates_path = os.path.join(root_path, _start_args['jinja_templates'])
139 | _start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path),
140 | autoescape=select_autoescape(['html', 'xml']))
141 |
142 | # Launch the browser to the starting URLs
143 | show(*start_urls)
144 |
145 | if _start_args['all_interfaces'] is True:
146 | HOST = '0.0.0.0'
147 | else:
148 | HOST = _start_args['host']
149 | # start web server (non blocking)
150 | app = web.Application()
151 | app.add_routes(routes)
152 | runner = web.AppRunner(app)
153 | await runner.setup()
154 | site = web.TCPSite(runner, host=HOST, port=_start_args['port'])
155 | await site.start()
156 | log.info(f"start http server {HOST}:{_start_args['port']}")
157 | except Exception:
158 | log.debug("http server exception", exc_info=True)
159 |
160 |
161 | def show(*start_urls):
162 | browsers.open(start_urls, _start_args)
163 |
164 |
165 | def spawn(function, *args, **kwargs):
166 | if asyncio.iscoroutinefunction(function):
167 | asyncio.ensure_future(function(*args, **kwargs))
168 | else:
169 | if 0 < len(kwargs):
170 | raise Exception('cannot convey kwargs')
171 | loop.call_soon_threadsafe(function, *args)
172 |
173 | # Bottle Routes
174 |
175 |
176 | @routes.get('/eel.js')
177 | async def _eel(request: BaseRequest):
178 | start_geometry = {'default': {'size': _start_args['size'],
179 | 'position': _start_args['position']},
180 | 'pages': _start_args['geometry']}
181 |
182 | page = _eel_js.replace('/** _py_functions **/',
183 | '_py_functions: %s,' % list(_exposed_functions.keys()))
184 | page = page.replace('/** _start_geometry **/',
185 | '_start_geometry: %s,' % _safe_json(start_geometry))
186 |
187 | response = web.Response(text=page, content_type='application/javascript')
188 | _set_response_headers(response)
189 | return response
190 |
191 |
192 | @routes.get('/eel')
193 | async def _websocket(request: BaseRequest):
194 |
195 | ws = await websocket_protocol_check(request)
196 |
197 | for js_function in _js_functions:
198 | _import_js_function(js_function)
199 |
200 | page = request.query.get('page')
201 | if page not in _mock_queue_done:
202 | for call in _mock_queue:
203 | await _repeated_send(ws, _safe_json(call))
204 | _mock_queue_done.add(page)
205 |
206 | _websockets.append((page, ws))
207 |
208 | while True:
209 | try:
210 | message = await ws.receive_json(timeout=0.1)
211 | asyncio.create_task(_process_message(message, ws))
212 | except (asyncio.TimeoutError, TypeError):
213 | if ws.closed:
214 | break
215 | except Exception:
216 | log.debug("WebSocket exception", exc_info=True)
217 | break
218 | # closed
219 | if not ws.closed:
220 | await ws.close()
221 | _websockets.remove((page, ws))
222 | _websocket_close(page)
223 |
224 |
225 | @routes.get('/{path:.*}')
226 | async def _static(request: BaseRequest):
227 | response = None
228 | try:
229 | path = request.path[1:]
230 | if 'jinja_env' in _start_args and 'jinja_templates' in _start_args:
231 | template_prefix = _start_args['jinja_templates'] + '/'
232 | if path.startswith(template_prefix):
233 | n = len(template_prefix)
234 | template = _start_args['jinja_env'].get_template(path[n:])
235 | response = web.Response(body=template.render(), content_type='text/html')
236 | else:
237 | file_path = os.path.join(root_path, path)
238 | if not os.path.isfile(file_path):
239 | return web.Response(text=f"not found {path}", status=404)
240 | log.debug(f"static access to '{path}'")
241 | response = web.FileResponse(path=file_path)
242 | except Exception as e:
243 | log.debug("http page exception", exc_info=True)
244 | response = web.Response(text=str(e), status=500)
245 |
246 | _set_response_headers(response)
247 | return response
248 |
249 |
250 | # Private functions
251 |
252 |
253 | def _safe_json(obj):
254 | return json.dumps(obj, default=lambda o: None)
255 |
256 |
257 | async def _repeated_send(ws: WebSocketResponse, msg):
258 | for attempt in range(100):
259 | try:
260 | await ws.send_str(msg)
261 | break
262 | except Exception:
263 | await asyncio.sleep(0.001)
264 |
265 |
266 | async def _process_message(message, ws: WebSocketResponse):
267 | if 'call' in message:
268 | function = _exposed_functions[message['name']]
269 | if asyncio.iscoroutinefunction(function):
270 | return_val = await function(*message['args'])
271 | else:
272 | return_val = function(*message['args'])
273 | await _repeated_send(ws, _safe_json({
274 | 'return': message['call'],
275 | 'value': return_val,
276 | }))
277 | elif 'return' in message:
278 | call_id = message['return']
279 | if call_id in _call_return_futures:
280 | future = _call_return_futures[call_id]
281 | if not future.done():
282 | future.set_result(message['value'])
283 | else:
284 | log.warning('Invalid message received: ', message)
285 |
286 |
287 | def _get_real_path(path):
288 | if getattr(sys, 'frozen', False):
289 | return os.path.join(sys._MEIPASS, path)
290 | else:
291 | return os.path.abspath(path)
292 |
293 |
294 | def _mock_js_function(f):
295 | """add globals awaitable function"""
296 | assert isinstance(f, str)
297 | globals()[f] = lambda *args: _mock_call(f, args)
298 | # exec('%s = lambda *args: _mock_call("%s", args)' % (f, f), globals())
299 |
300 |
301 | def _import_js_function(f):
302 | """add globals awaitable function"""
303 | assert isinstance(f, str)
304 | globals()[f] = lambda *args: _js_call(f, args)
305 | # exec('%s = lambda *args: _js_call("%s", args)' % (f, f), globals())
306 |
307 |
308 | def _call_object(name, args):
309 | global _call_number
310 | _call_number += 1
311 | call_id = _call_number + random()
312 | return {'call': call_id, 'name': name, 'args': args}
313 |
314 |
315 | def _mock_call(name, args):
316 | call_object = _call_object(name, args)
317 | _mock_queue.append(call_object)
318 | return _call_return(call_object['call'])
319 |
320 |
321 | def _js_call(name, args):
322 | call_object = _call_object(name, args)
323 | data = _safe_json(call_object)
324 | for _, ws in _websockets:
325 | asyncio.ensure_future(_repeated_send(ws, data))
326 | return _call_return(call_object['call'])
327 |
328 |
329 | def _call_return(call_id):
330 | future = asyncio.Future()
331 | _call_return_futures[call_id] = future
332 |
333 | async def wait_for_result(callback):
334 | """wait for result and exc callback"""
335 | try:
336 | await asyncio.wait_for(future, timeout=10.0)
337 | args = future.result()
338 | if asyncio.iscoroutinefunction(callback):
339 | await callback(args)
340 | else:
341 | callback(args)
342 | except asyncio.TimeoutError:
343 | log.debug("timeout on wait_for_result")
344 | except Exception:
345 | log.debug("_call_return exception", exc_info=True)
346 |
347 | async def return_func(callback=None):
348 | """return data or task object"""
349 | try:
350 | if callback is None:
351 | await asyncio.wait_for(future, timeout=10.0)
352 | return future.result()
353 | else:
354 | return asyncio.ensure_future(wait_for_result(callback))
355 | except asyncio.TimeoutError:
356 | log.debug("timeout on return_func")
357 | except Exception:
358 | log.debug("return_func exception", exc_info=True)
359 | return return_func
360 |
361 |
362 | def _expose(name, function):
363 | msg = 'Already exposed function with name "%s"' % name
364 | assert name not in _exposed_functions, msg
365 | _exposed_functions[name] = function
366 |
367 |
368 | def _websocket_close(page):
369 | close_callback: Optional[Callable] = _start_args.get('close_callback')
370 |
371 | if close_callback is not None:
372 | web_sockets = [ws for _, ws in _websockets]
373 | close_callback(page, web_sockets)
374 | else:
375 | # Default behaviour - wait 1s, then quit if all sockets are closed
376 | if _start_args['auto_close'] and len(_websockets) == 0:
377 | loop.call_later(1.0, loop.stop)
378 |
379 |
380 | def _set_response_headers(response):
381 | if _start_args['disable_cache']:
382 | # https://stackoverflow.com/a/24748094/280852
383 | response.headers.add('Cache-Control', 'no-store')
384 |
385 |
386 | __all__ = [
387 | "expose",
388 | "init",
389 | "start",
390 | "show",
391 | "spawn",
392 | ]
393 |
--------------------------------------------------------------------------------
/async_eel/__main__.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import sys
3 | import pkg_resources as pkg
4 | import PyInstaller.__main__ as pyi
5 | import os
6 |
7 | args = sys.argv[1:]
8 | main_script = args.pop(0)
9 | web_folder = args.pop(0)
10 |
11 | print("Building executable with main script '%s' and web folder '%s'...\n" %
12 | (main_script, web_folder))
13 |
14 | eel_js_file = pkg.resource_filename('eel', 'eel.js')
15 | js_file_arg = '%s%seel' % (eel_js_file, os.pathsep)
16 | web_folder_arg = '%s%s%s' % (web_folder, os.pathsep, web_folder)
17 |
18 | needed_args = ['--hidden-import', 'bottle_websocket',
19 | '--add-data', js_file_arg, '--add-data', web_folder_arg]
20 | full_args = [main_script] + needed_args + args
21 |
22 | print('Running:\npyinstaller', ' '.join(full_args), '\n')
23 |
24 | pyi.run(full_args)
25 |
--------------------------------------------------------------------------------
/async_eel/browsers.py:
--------------------------------------------------------------------------------
1 | import subprocess as sps
2 | import webbrowser as wbr
3 |
4 | import async_eel.chrome as chm
5 | import async_eel.electron as ele
6 | import async_eel.edge as edge
7 | #import async_eel.firefox as ffx TODO
8 | #import async_eel.safari as saf TODO
9 |
10 | _browser_paths = {}
11 | _browser_modules = {'chrome': chm,
12 | 'electron': ele,
13 | 'edge': edge}
14 |
15 |
16 | def _build_url_from_dict(page, options):
17 | scheme = page.get('scheme', 'http')
18 | host = page.get('host', 'localhost')
19 | port = page.get('port', 8000)
20 | path = page.get('path', '')
21 | return '%s://%s:%d/%s' % (scheme, host, port, path)
22 |
23 |
24 | def _build_url_from_string(page, options):
25 | base_url = 'http://%s:%d/' % (options['host'], options['port'])
26 | return base_url + page
27 |
28 |
29 | def _build_urls(start_pages, options):
30 | urls = []
31 |
32 | for page in start_pages:
33 | method = _build_url_from_dict if isinstance(
34 | page, dict) else _build_url_from_string
35 | url = method(page, options)
36 | urls.append(url)
37 |
38 | return urls
39 |
40 |
41 | def open(start_pages, options):
42 | # Build full URLs for starting pages (including host and port)
43 | start_urls = _build_urls(start_pages, options)
44 |
45 | mode = options.get('mode')
46 | if mode in [None, False]:
47 | # Don't open a browser
48 | pass
49 | elif mode == 'custom':
50 | # Just run whatever command the user provided
51 | sps.Popen(options['cmdline_args'],
52 | stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE)
53 | elif mode in _browser_modules:
54 | # Run with a specific browser
55 | browser_module = _browser_modules[mode]
56 | path = _browser_paths.get(mode)
57 | if path is None:
58 | # Don't know this browser's path, try and find it ourselves
59 | path = browser_module.find_path()
60 | _browser_paths[mode] = path
61 |
62 | if path is not None:
63 | browser_module.run(path, options, start_urls)
64 | else:
65 | raise EnvironmentError("Can't find %s installation" % browser_module.name)
66 | else:
67 | # Fall back to system default browser
68 | for url in start_urls:
69 | wbr.open(url)
70 |
71 |
72 | def set_path(browser_name, path):
73 | _browser_paths[browser_name] = path
74 |
75 |
76 | def get_path(browser_name):
77 | return _browser_paths.get(browser_name)
78 |
79 |
--------------------------------------------------------------------------------
/async_eel/chrome.py:
--------------------------------------------------------------------------------
1 | import sys, subprocess as sps, os
2 |
3 | # Every browser specific module must define run(), find_path() and name like this
4 |
5 | name = 'Google Chrome/Chromium'
6 |
7 | def run(path, options, start_urls):
8 | if options['app_mode']:
9 | for url in start_urls:
10 | sps.Popen([path, '--app=%s' % url] +
11 | options['cmdline_args'],
12 | stdout=sps.PIPE, stderr=sps.PIPE, stdin=sps.PIPE)
13 | else:
14 | args = options['cmdline_args'] + start_urls
15 | sps.Popen([path, '--new-window'] + args,
16 | stdout=sps.PIPE, stderr=sys.stderr, stdin=sps.PIPE)
17 |
18 |
19 | def find_path():
20 | if sys.platform in ['win32', 'win64']:
21 | return _find_chrome_win()
22 | elif sys.platform == 'darwin':
23 | return _find_chrome_mac()
24 | elif sys.platform.startswith('linux'):
25 | return _find_chrome_linux()
26 | else:
27 | return None
28 |
29 |
30 | def _find_chrome_mac():
31 | default_dir = r'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
32 | if os.path.exists(default_dir):
33 | return default_dir
34 | # use mdfind ci to locate Chrome in alternate locations and return the first one
35 | name = 'Google Chrome.app'
36 | alternate_dirs = [x for x in sps.check_output(["mdfind", name]).decode().split('\n') if x.endswith(name)]
37 | if len(alternate_dirs):
38 | return alternate_dirs[0] + '/Contents/MacOS/Google Chrome'
39 | return None
40 |
41 |
42 | def _find_chrome_linux():
43 | import whichcraft as wch
44 | chrome_names = ['chromium-browser',
45 | 'chromium',
46 | 'google-chrome',
47 | 'google-chrome-stable']
48 |
49 | for name in chrome_names:
50 | chrome = wch.which(name)
51 | if chrome is not None:
52 | return chrome
53 | return None
54 |
55 |
56 | def _find_chrome_win():
57 | import winreg as reg
58 | reg_path = r'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe'
59 |
60 | for install_type in reg.HKEY_CURRENT_USER, reg.HKEY_LOCAL_MACHINE:
61 | try:
62 | reg_key = reg.OpenKey(install_type, reg_path, 0, reg.KEY_READ)
63 | chrome_path = reg.QueryValue(reg_key, None)
64 | reg_key.Close()
65 | except WindowsError:
66 | chrome_path = None
67 | else:
68 | break
69 |
70 | return chrome_path
71 |
--------------------------------------------------------------------------------
/async_eel/edge.py:
--------------------------------------------------------------------------------
1 | import subprocess as sps
2 | import sys
3 |
4 | name = 'Edge'
5 |
6 |
7 | def run(_path, options, start_urls):
8 | cmd = 'start microsoft-edge:{}'.format(start_urls[0])
9 | sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE, shell=True)
10 |
11 |
12 | def find_path():
13 | # Path isn't necessary. Edge is launched with a CLI argument
14 | return True
15 |
--------------------------------------------------------------------------------
/async_eel/eel.js:
--------------------------------------------------------------------------------
1 | eel = {
2 | _host: window.location.origin,
3 |
4 | set_host: function (hostname) {
5 | eel._host = hostname
6 | },
7 |
8 | expose: function(f, name) {
9 | if(name === undefined){
10 | name = f.toString();
11 | let i = 'function '.length, j = name.indexOf('(');
12 | name = name.substring(i, j).trim();
13 | }
14 |
15 | eel._exposed_functions[name] = f;
16 | },
17 |
18 | guid: function() {
19 | return eel._guid;
20 | },
21 |
22 | // These get dynamically added by library when file is served
23 | /** _py_functions **/
24 | /** _start_geometry **/
25 |
26 | _guid: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
27 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
28 | ),
29 |
30 | _exposed_functions: {},
31 |
32 | _mock_queue: [],
33 |
34 | _mock_py_functions: function() {
35 | for(let i = 0; i < eel._py_functions.length; i++) {
36 | let name = eel._py_functions[i];
37 | eel[name] = function() {
38 | let call_object = eel._call_object(name, arguments);
39 | eel._mock_queue.push(call_object);
40 | return eel._call_return(call_object);
41 | }
42 | }
43 | },
44 |
45 | _import_py_function: function(name) {
46 | let func_name = name;
47 | eel[name] = function() {
48 | let call_object = eel._call_object(func_name, arguments);
49 | eel._websocket.send(eel._toJSON(call_object));
50 | return eel._call_return(call_object);
51 | }
52 | },
53 |
54 | _call_number: 0,
55 |
56 | _call_return_callbacks: {},
57 |
58 | _call_object: function(name, args) {
59 | let arg_array = [];
60 | for(let i = 0; i < args.length; i++){
61 | arg_array.push(args[i]);
62 | }
63 |
64 | let call_id = (eel._call_number += 1) + Math.random();
65 | return {'call': call_id, 'name': name, 'args': arg_array};
66 | },
67 |
68 | _sleep: function(ms) {
69 | return new Promise(resolve => setTimeout(resolve, ms));
70 | },
71 |
72 | _toJSON: function(obj) {
73 | return JSON.stringify(obj, (k, v) => v === undefined ? null : v);
74 | },
75 |
76 | _call_return: function(call) {
77 | return function(callback = null) {
78 | if(callback != null) {
79 | eel._call_return_callbacks[call.call] = callback;
80 | } else {
81 | return new Promise(function(resolve) {
82 | eel._call_return_callbacks[call.call] = resolve;
83 | });
84 | }
85 | }
86 | },
87 |
88 | _position_window: function(page) {
89 | let size = eel._start_geometry['default'].size;
90 | let position = eel._start_geometry['default'].position;
91 |
92 | if(page in eel._start_geometry.pages) {
93 | size = eel._start_geometry.pages[page].size;
94 | position = eel._start_geometry.pages[page].position;
95 | }
96 |
97 | if(size != null){
98 | window.resizeTo(size[0], size[1]);
99 | }
100 |
101 | if(position != null){
102 | window.moveTo(position[0], position[1]);
103 | }
104 | },
105 |
106 | _init: function() {
107 | eel._mock_py_functions();
108 |
109 | document.addEventListener("DOMContentLoaded", function(event) {
110 | let page = window.location.pathname.substring(1);
111 | eel._position_window(page);
112 |
113 | let websocket_addr = (eel._host + '/eel').replace('http', 'ws');
114 | websocket_addr += ('?page=' + page);
115 | eel._websocket = new WebSocket(websocket_addr);
116 |
117 | eel._websocket.onopen = function() {
118 | for(let i = 0; i < eel._py_functions.length; i++){
119 | let py_function = eel._py_functions[i];
120 | eel._import_py_function(py_function);
121 | }
122 |
123 | while(eel._mock_queue.length > 0) {
124 | let call = eel._mock_queue.shift();
125 | eel._websocket.send(eel._toJSON(call));
126 | }
127 | };
128 |
129 | eel._websocket.onmessage = function (e) {
130 | let message = JSON.parse(e.data);
131 | if(message.hasOwnProperty('call') ) {
132 | // Python making a function call into us
133 | if(message.name in eel._exposed_functions) {
134 | let return_val = eel._exposed_functions[message.name](...message.args);
135 | eel._websocket.send(eel._toJSON({'return': message.call, 'value': return_val}));
136 | }
137 | } else if(message.hasOwnProperty('return')) {
138 | // Python returning a value to us
139 | if(message['return'] in eel._call_return_callbacks) {
140 | eel._call_return_callbacks[message['return']](message.value);
141 | }
142 | } else {
143 | throw 'Invalid message ' + message;
144 | }
145 |
146 | };
147 | });
148 | }
149 | }
150 |
151 | eel._init();
152 |
153 | if(typeof require !== 'undefined'){
154 | // Avoid name collisions when using Electron, so jQuery etc work normally
155 | window.nodeRequire = require;
156 | delete window.require;
157 | delete window.exports;
158 | delete window.module;
159 | }
160 |
--------------------------------------------------------------------------------
/async_eel/electron.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import subprocess as sps
4 | import whichcraft as wch
5 |
6 | name = 'Electron'
7 |
8 | def run(path, options, start_urls):
9 | cmd = [path] + options['cmdline_args']
10 | cmd += ['.', ';'.join(start_urls)]
11 | sps.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, stdin=sps.PIPE)
12 |
13 |
14 | def find_path():
15 | if sys.platform in ['win32', 'win64']:
16 | # It doesn't work well passing the .bat file to Popen, so we get the actual .exe
17 | bat_path = wch.which('electron')
18 | return os.path.join(bat_path, r'..\node_modules\electron\dist\electron.exe')
19 | elif sys.platform in ['darwin', 'linux']:
20 | # This should work find...
21 | return wch.which('electron')
22 | else:
23 | return None
24 |
25 |
--------------------------------------------------------------------------------
/async_eel/utils.py:
--------------------------------------------------------------------------------
1 | from aiohttp.web import BaseRequest
2 | from aiohttp import web
3 | from logging import getLogger
4 |
5 |
6 | log = getLogger(__name__)
7 |
8 |
9 | async def websocket_protocol_check(request: BaseRequest):
10 | """protocol upgrade to WebSocket"""
11 | ws = web.WebSocketResponse()
12 | ws.enable_compression()
13 | available = ws.can_prepare(request)
14 | if not available:
15 | raise TypeError('cannot prepare websocket')
16 | await ws.prepare(request)
17 | log.debug(f"protocol upgrade to websocket protocol {request.remote}")
18 | return ws
19 |
20 |
21 | __all__ = [
22 | "websocket_protocol_check",
23 | ]
24 |
--------------------------------------------------------------------------------
/examples/01 - hello_world-Edge/hello.py:
--------------------------------------------------------------------------------
1 | import os
2 | import platform
3 | import sys
4 | import asyncio
5 | # Use latest version of Eel from parent directory
6 | sys.path.insert(1, '../../')
7 | import async_eel
8 |
9 |
10 | loop = asyncio.get_event_loop()
11 |
12 |
13 | @async_eel.expose # Expose this function to Javascript
14 | async def say_hello_py(x):
15 | print('Hello from %s' % x)
16 |
17 |
18 | async def main():
19 | # Use the same static files as the original Example
20 | os.chdir(os.path.join('..', '01 - hello_world'))
21 |
22 | # Set web files folder and optionally specify which file types to check for eel.expose()
23 | async_eel.init('web', allowed_extensions=['.js', '.html'])
24 |
25 | # Launch example in Microsoft Edge only on Windows 10 and above
26 | if sys.platform in ['win32', 'win64'] and int(platform.release()) >= 10:
27 | await async_eel.start('hello.html', mode='edge')
28 | else:
29 | raise EnvironmentError('Error: System is not Windows 10 or above')
30 |
31 | # # Launching Edge can also be gracefully handled as a fall back
32 | # try:
33 | # await async_eel.start('hello.html', mode='chrome-app', size=(300, 200))
34 | # except EnvironmentError:
35 | # # If Chrome isn't found, fallback to Microsoft Edge on Win10 or greater
36 | # if sys.platform in ['win32', 'win64'] and int(platform.release()) >= 10:
37 | # await async_eel.start('hello.html', mode='edge')
38 | # else:
39 | # raise
40 |
41 | await say_hello_py('Python World!')
42 | await async_eel.say_hello_js('Python World!')() # Call a Javascript function
43 | print("OK")
44 |
45 |
46 | if __name__ == '__main__':
47 | asyncio.run_coroutine_threadsafe(main(), loop)
48 | loop.run_forever()
49 |
--------------------------------------------------------------------------------
/examples/01 - hello_world/hello.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function # For Py2/3 compatibility
2 | import async_eel
3 | import asyncio
4 |
5 | loop = asyncio.get_event_loop()
6 |
7 |
8 | @async_eel.expose # Expose this function to Javascript
9 | async def say_hello_py(x):
10 | print('Hello from %s' % x)
11 |
12 |
13 | async def main():
14 | # Set web files folder
15 | async_eel.init('web')
16 | await async_eel.start('hello.html', size=(300, 200)) # Start
17 |
18 | await say_hello_py('Python World!')
19 | await async_eel.say_hello_js('Python World!')() # Call a Javascript function
20 | print("OK")
21 |
22 |
23 | if __name__ == '__main__':
24 | asyncio.run_coroutine_threadsafe(main(), loop)
25 | loop.run_forever()
26 |
27 |
--------------------------------------------------------------------------------
/examples/01 - hello_world/web/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/namuyan/async-Eel/141e49e2ea7f1d33565df281bac418526b9d86cd/examples/01 - hello_world/web/favicon.ico
--------------------------------------------------------------------------------
/examples/01 - hello_world/web/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello, World!
5 |
6 |
7 |
8 |
19 |
20 |
21 |
22 | Hello, World!
23 |
24 |
--------------------------------------------------------------------------------
/examples/02 - callbacks/callbacks.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function # For Py2/3 compatibility
2 | import async_eel
3 | import random
4 | import asyncio
5 |
6 |
7 | loop = asyncio.get_event_loop()
8 |
9 |
10 | @async_eel.expose
11 | async def py_random():
12 | return random.random()
13 |
14 |
15 | async def print_num(n):
16 | """callback of js_random"""
17 | print('Got this from Javascript:', n)
18 |
19 |
20 | async def main():
21 | try:
22 | async_eel.init('web')
23 | await async_eel.start('callbacks.html', size=(400, 300))
24 |
25 | # Call Javascript function, and pass explicit callback function
26 | await async_eel.js_random()(print_num)
27 |
28 | # Do the same with an inline callback
29 | await async_eel.js_random()(lambda n: print('2Got this from Javascript:', n))
30 | except Exception:
31 | import traceback
32 | traceback.print_exc()
33 |
34 |
35 | if __name__ == '__main__':
36 | asyncio.run_coroutine_threadsafe(main(), loop)
37 | loop.run_forever()
38 |
--------------------------------------------------------------------------------
/examples/02 - callbacks/web/callbacks.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Callbacks Demo
5 |
6 |
7 |
25 |
26 |
27 |
28 | Callbacks demo
29 |
30 |
--------------------------------------------------------------------------------
/examples/02 - callbacks/web/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/namuyan/async-Eel/141e49e2ea7f1d33565df281bac418526b9d86cd/examples/02 - callbacks/web/favicon.ico
--------------------------------------------------------------------------------
/examples/03 - sync_callbacks/sync_callbacks.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function # For Py2/3 compatibility
2 | import async_eel, random
3 | import asyncio
4 |
5 | loop = asyncio.get_event_loop()
6 |
7 |
8 | @async_eel.expose
9 | def py_random():
10 | return random.random()
11 |
12 |
13 | async def main():
14 | async_eel.init('web')
15 | await async_eel.start('sync_callbacks.html', block=False, size=(400, 300))
16 | # Synchronous calls must happen after start() is called
17 |
18 | # Get result returned synchronously by
19 | # passing nothing in second brackets
20 | # v
21 | data = await async_eel.js_random()()
22 | print('Got this from Javascript:', data)
23 |
24 |
25 | if __name__ == '__main__':
26 | asyncio.run_coroutine_threadsafe(main(), loop)
27 | loop.run_forever()
28 |
--------------------------------------------------------------------------------
/examples/03 - sync_callbacks/web/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/namuyan/async-Eel/141e49e2ea7f1d33565df281bac418526b9d86cd/examples/03 - sync_callbacks/web/favicon.ico
--------------------------------------------------------------------------------
/examples/03 - sync_callbacks/web/sync_callbacks.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Synchronous callbacks
5 |
6 |
7 |
27 |
28 |
29 |
30 | Synchronous callbacks
31 |
32 |
--------------------------------------------------------------------------------
/examples/04 - file_access/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/namuyan/async-Eel/141e49e2ea7f1d33565df281bac418526b9d86cd/examples/04 - file_access/Screenshot.png
--------------------------------------------------------------------------------
/examples/04 - file_access/file_access.py:
--------------------------------------------------------------------------------
1 | import async_eel, os, random
2 | import asyncio
3 |
4 |
5 | loop = asyncio.get_event_loop()
6 |
7 |
8 | @async_eel.expose
9 | def pick_file(folder):
10 | if os.path.isdir(folder):
11 | return random.choice(os.listdir(folder))
12 | else:
13 | return 'Not valid folder'
14 |
15 |
16 | async def main():
17 | async_eel.init('web')
18 | await async_eel.start('file_access.html', size=(320, 120))
19 |
20 |
21 | if __name__ == '__main__':
22 | asyncio.run_coroutine_threadsafe(main(), loop)
23 | loop.run_forever()
24 |
--------------------------------------------------------------------------------
/examples/04 - file_access/web/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/namuyan/async-Eel/141e49e2ea7f1d33565df281bac418526b9d86cd/examples/04 - file_access/web/favicon.ico
--------------------------------------------------------------------------------
/examples/04 - file_access/web/file_access.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Eel Demo
5 |
6 |
18 |
19 |
20 |
21 |
22 | Pick random file
23 |