Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add demo for concurrent web clients #10

Merged
merged 39 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bf26dbf
Update with complete demo
garrettmflynn Jan 19, 2024
9131def
Update publisher.py
garrettmflynn Jan 19, 2024
3774370
Update client.html
garrettmflynn Jan 19, 2024
41c1bb9
Delete main.py
garrettmflynn Jan 19, 2024
0c01c1d
Update README.md
garrettmflynn Jan 19, 2024
778d677
Add .toml file to replace setup.py
garrettmflynn Jan 19, 2024
98e7be7
Add numpy-style docstrings
garrettmflynn Jan 19, 2024
9719e34
Update license to file
garrettmflynn Jan 19, 2024
d09921d
Update pyproject.toml
garrettmflynn Jan 19, 2024
cde3b30
Switch back from hex
garrettmflynn Jan 19, 2024
dbd0bcc
Update README.md
garrettmflynn Jan 19, 2024
82048a0
Create requirements.txt
garrettmflynn Jan 19, 2024
638b5a7
Update requirements.txt
garrettmflynn Jan 19, 2024
ad2cede
Rename
garrettmflynn Jan 19, 2024
1b4a91c
Update pyproject.toml
garrettmflynn Jan 19, 2024
ebddc12
Merge branch 'main' into demo
CodyCBakerPhD Jan 19, 2024
5fd6432
Use uuid string
garrettmflynn Jan 19, 2024
f3f2c6a
Merge branch 'demo' of https://github.com/catalystneuro/tqdm_publishe…
garrettmflynn Jan 19, 2024
6ccff8f
Update publisher.py
garrettmflynn Jan 19, 2024
c9298e1
Update requirements.txt
garrettmflynn Jan 19, 2024
14a311c
Update pyproject.toml
garrettmflynn Jan 19, 2024
1ae7682
Merge branch 'pyproject' into demo
garrettmflynn Jan 19, 2024
b864928
Add CLI script
garrettmflynn Jan 19, 2024
054589b
Update pyproject.toml
garrettmflynn Jan 19, 2024
f8cd0b1
Update pyproject.toml
garrettmflynn Jan 19, 2024
c9535d4
Move requirements into toml file
garrettmflynn Jan 19, 2024
27bd991
Merge branch 'pyproject' into demo
garrettmflynn Jan 19, 2024
0629ac5
Apply suggestions from code review
garrettmflynn Jan 19, 2024
e4ba9f4
Update pyproject.toml
garrettmflynn Jan 19, 2024
cd4d346
Merge branch 'pyproject' into demo
garrettmflynn Jan 19, 2024
8929876
Update publisher.py
garrettmflynn Jan 19, 2024
82b7280
Remove display check
garrettmflynn Jan 19, 2024
bbc5c4c
Improve CLI
garrettmflynn Jan 19, 2024
63b7a40
Merge branch 'main' into demo
CodyCBakerPhD Jan 19, 2024
03356a4
Updated with a base command check
garrettmflynn Jan 19, 2024
2731c2d
Merge branch 'demo' of https://github.com/catalystneuro/tqdm_publishe…
garrettmflynn Jan 19, 2024
423b0fc
Update publisher.py
garrettmflynn Jan 19, 2024
14d5cf3
Update publisher.py
garrettmflynn Jan 19, 2024
1e6c193
Merge branch 'docstrings' into demo
garrettmflynn Jan 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,20 @@ n = 10**5
sleep_durations = [random.uniform(0, 5.0) for _ in range(n)]
asyncio.run(run_multiple_sleeps(sleep_durations=sleep_durations))
```

## Demo
A complete demo of `tqdm_publisher` can be found in the `demo` directory, which shows how to forward progress updates from the same `TQDMPublisher` instance to multiple clients.

To run the demo, first install the dependencies:
```bash
pip install tqdm_publisher[demo]
```

Then, run the base CLI command to start the demo server and client:
```bash
tqdm_publisher demo
```

> **Note:** Alternatively, you can run each part of the demo separately by running `tqdm_publisher demo --server` and `tqdm_publisher demo --client` in separate terminals.

Finally, you can click the Create Progress Bar button to create a new `TQDMPublisher` instance, which will begin updating based on the `TQDMPublisher` instance in the Python script.
37 changes: 37 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import subprocess
import sys
from pathlib import Path

demo_base_path = Path(__file__).parent / "demo"

client_path = demo_base_path / "client.html"
server_path = demo_base_path/ "server.py"

def main():

command = sys.argv[1]
flags_list = sys.argv[2:]

client_flag = "--client" in flags_list
server_flag = "--server" in flags_list
both_flags = "--server" in flags_list and "--client" in flags_list

flags = dict(
client = not server_flag or both_flags,
server = not client_flag or both_flags,

)

if (command == "demo"):
if flags["client"]:
subprocess.run(["open", client_path])

if flags["server"]:
subprocess.run(["python", server_path])

else:
print(f"{command} is an invalid command.")


if __name__ == "__main__":
main()
110 changes: 110 additions & 0 deletions demo/client.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Concurrent Client Demo</title>

<style>

html, body {
font-family: sans-serif;
}

h1 {
margin: 0;
padding: 0;
font-size: 1.5rem;
}

header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
}

#bars {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}

.progress {
width: 100%;
height: 20px;
background-color: #ddd;
}



.progress div {
height: 100%;
background-color: #4caf50;
width: 0%;
}

</style>

</head>

<body>
<header>
<div>
<h1>tqdm_progress</h1>
<i><small>Create multiple progress bars to test concurrent subscriptions</small></i>
</div>
<button>Create Progress Bar</button>
</header>

<div id="bars">

</div>

</body>

<script>

const bars = document.querySelector('#bars');

class ProgressClient {
constructor() {
this.socket = new WebSocket('ws://localhost:8000');

this.element = document.createElement('div');
this.element.classList.add('progress');
const progress = document.createElement('div');
this.element.appendChild(progress);
bars.appendChild(this.element);

this.socket.addEventListener('message', function (event) {
const data = JSON.parse(event.data);
progress.style.width = 100 * (data.n / data.total) + '%';
});

}

close() {
this.socket.close();
this.element.remove()
}

}

const button = document.querySelector('button');
button.addEventListener('click', () => {
const client = new ProgressClient();
})

</script>

</html>
11 changes: 11 additions & 0 deletions demo/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import subprocess

from pathlib import Path

client_path = Path(__file__).parent / "client.html"

def main():
subprocess.run(["open", client_path])

if __name__ == "__main__":
main()
160 changes: 160 additions & 0 deletions demo/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env python

import random
import asyncio
from typing import List

from tqdm_publisher import TQDMPublisher

import websockets
import threading
from uuid import uuid4

import json

async def sleep_func(sleep_duration: float = 1) -> float:
await asyncio.sleep(delay=sleep_duration)


def create_tasks():
n = 10**5
sleep_durations = [random.uniform(0, 5.0) for _ in range(n)]
tasks = []

for sleep_duration in sleep_durations:
task = asyncio.create_task(sleep_func(sleep_duration=sleep_duration))
tasks.append(task)

return tasks


class ProgressHandler():

def __init__(self):
self.started = False
self.callbacks = []
self.callback_ids = []

def subscribe(self, callback):
self.callbacks.append(callback)

if (hasattr(self, 'progress_bar')):
self._subscribe(callback)


def unsubscribe(self, callback_id):
self.progress_bar.unsubscribe(callback_id)

def clear(self):
self.callbacks = []
self._clear()

def _clear(self):

for callback_id in self.callback_ids:
self.unsubscribe(callback_id)

self.callback_ids = []

async def run(self):
for f in self.progress_bar:
await f

def stop(self):
self.started = False
self.clear()
self.thread.join()


def _subscribe(self, callback):
callback_id = self.progress_bar.subscribe(callback)
self.callback_ids.append(callback_id)


async def run(self):

if (hasattr(self, 'progress_bar')):
print("Progress bar already running")
return

self.tasks = create_tasks()
self.progress_bar = TQDMPublisher(asyncio.as_completed(self.tasks), total=len(self.tasks))

for callback in self.callbacks:
self._subscribe(callback)

for f in self.progress_bar:
await f

self._clear()
del self.progress_bar



def thread_loop(self):
while self.started:
asyncio.run(self.run())

def start(self):

if (self.started):
return

self.started = True

self.thread = threading.Thread(target=self.thread_loop) # Start infinite loop of progress bar thread
self.thread.start()


progress_handler = ProgressHandler()

class WebSocketHandler:
def __init__(self):

self.clients = {}

# Initialize with any state you need
pass

def handle_task_result(self, task):
try:
task.result() # This will re-raise any exception that occurred in the task
except websockets.exceptions.ConnectionClosedOK:
print("WebSocket closed while sending message")
except Exception as e:
print(f"Error in task: {e}")

async def handler(self, websocket):
id = str(uuid4())
self.clients[id] = websocket # Register client connection

progress_handler.start() # Start if not started

def on_progress(info):
task = asyncio.create_task(websocket.send(json.dumps(info)))
task.add_done_callback(self.handle_task_result) # Handle task result or exception

progress_handler.subscribe(on_progress)

try:
async for message in websocket:
print("Message from client received:", message)

finally:
# This is called when the connection is closed
del self.clients[id]
if (len(self.clients) == 0):
progress_handler.stop()



async def spawn_server():
handler = WebSocketHandler().handler
async with websockets.serve(handler, "", 8000):
await asyncio.Future() # run forever

def main():
asyncio.run(spawn_server())

if __name__ == "__main__":
main()
30 changes: 0 additions & 30 deletions main.py

This file was deleted.

Loading