Browse Source

Merge branch 'zauberzeug:main' into splitter

Andy Bulka 2 years ago
parent
commit
f276f7256c

+ 14 - 0
.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,14 @@
+blank_issues_enabled: true
+contact_links:
+  - name: Have an idea or a feature requests?
+    url: https://github.com/zauberzeug/nicegui/discussions/categories/ideas-feature-requests
+    about: Visit the "💡 Ideas / Feature Requests" forum to view, discuss, vote and propose ideas and feature suggestions.
+  - name: Need support from the developers?
+    url: https://github.com/zauberzeug/nicegui/discussions/categories/q-a
+    about: Visit the "🙏 Q&A" forum to find answers or to ask new questions to the developers.
+  - name: Ask on Discord
+    url: https://discord.gg/TEpFeAaF4f
+    about: Get in touch with the NiceGUI community, get support and have a chat about your project.
+  - name: Ask on StackOverflow
+    url: https://stackoverflow.com/questions/tagged/nicegui
+    about: Get help from a larger community by asking on StackOverflow.

+ 10 - 0
.github/ISSUE_TEMPLATE/report-an-issue.md

@@ -0,0 +1,10 @@
+---
+name: Report an issue
+about: Report a bug or some other unexpected behavior.
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+

+ 0 - 4
.github/workflows/publish.yml

@@ -23,10 +23,6 @@ jobs:
       - name: get version
         id: get_version
         run: echo "VERSION=$(echo ${GITHUB_REF/refs\/tags\//})" >> $GITHUB_ENV
-      - name: Update Citation
-        run: |
-          pip install PyYAML
-          python .github/workflows/update_citation.py
       - name: set version
         run: poetry version ${{ env.VERSION }}
       - name: publish

+ 29 - 12
.github/workflows/update_citation.py

@@ -1,17 +1,34 @@
-import yaml
 import os
-from datetime import datetime
+import sys
+
+import requests
+import yaml
 
-with open('CITATION.cff', 'r') as f:
-    citation = yaml.safe_load(f)
 
-citation['version'] = os.environ['GITHUB_REF'].split('/')[-1]
-citation['date-released'] = datetime.utcnow().strftime('%Y-%m-%d')
-# citation['doi'] = 'doi base (not ready yet)' + os.environ['GITHUB_REF'].split('/')[-1]
+def get_infos() -> str:
+    headers = {
+        'Accept': 'application/json',
+    }
+    params = {
+        'access_token': os.environ['ZENODO_TOKEN'],
+        'q': 'nicegui',
+        'sort': 'mostrecent',
+        'status': 'published',
+    }
+    try:
+        response = requests.get('https://zenodo.org/api/records', params=params, headers=headers)
+        response.raise_for_status()
+    # Hide all error details to avoid leaking the token
+    except Exception:
+        print('Error while getting the Zenodo infos')
+        sys.exit(1)
+    data = response.json()[0]['metadata']
+    return data['doi'], data['version'], data['publication_date']
 
-with open('CITATION.cff', 'w') as f:
-    yaml.dump(citation, f, sort_keys=False, default_flow_style=False)
 
-os.system('git add CITATION.cff')
-os.system('git commit -m "Update CITATION.cff for release"')
-os.system('git push')
+if __name__ == '__main__':
+    with open('CITATION.cff', 'r') as file:
+        citation = yaml.safe_load(file)
+    citation['doi'], citation['version'], citation['date-released'] = get_infos()
+    with open('CITATION.cff', 'w') as file:
+        yaml.dump(citation, file, sort_keys=False, default_flow_style=False)

+ 36 - 0
.github/workflows/update_citation.yml

@@ -0,0 +1,36 @@
+name: Update Citation
+
+on:
+  release:
+    types:
+      - published
+
+jobs:
+  update_citation:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.11
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install requests PyYAML
+
+      - name: Update Citation.cff
+        env:
+          ZENODO_TOKEN: ${{ secrets.ZENODO_TOKEN }}
+        run: python .github/workflows/update_citation.py
+
+      - name: Commit and push changes
+        run: |
+          git config --global user.name "github-actions[bot]"
+          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
+          git add CITATION.cff
+          git commit -m "Update citation.cff"
+          git push

+ 12 - 11
CITATION.cff

@@ -1,13 +1,14 @@
 cff-version: 1.2.0
-message: "If you use this software, please cite it as below."
+message: If you use this software, please cite it as below.
 authors:
-  - family-names: "Schindler"
-    given-names: "Falko"
-    orcid: "https://orcid.org/0009-0003-5359-835X"
-  - family-names: "Trappe"
-    given-names: "Rodja"
-    orcid: "https://orcid.org/0009-0009-4735-6227"
-title: "NiceGUI: Web-based interfaces with Python. The nice way."
-version: 1.2.2
-date-released: 2023-03-25
-url: "https://github.com/zauberzeug/nicegui"
+- family-names: Schindler
+  given-names: Falko
+  orcid: https://orcid.org/0009-0003-5359-835X
+- family-names: Trappe
+  given-names: Rodja
+  orcid: https://orcid.org/0009-0009-4735-6227
+title: 'NiceGUI: Web-based interfaces with Python. The nice way.'
+version: v1.2.3
+date-released: '2023-03-30'
+url: https://github.com/zauberzeug/nicegui
+doi: 10.5281/zenodo.7785517

+ 14 - 0
citation.cff

@@ -0,0 +1,14 @@
+cff-version: 1.2.0
+message: If you use this software, please cite it as below.
+authors:
+- family-names: Schindler
+  given-names: Falko
+  orcid: https://orcid.org/0009-0003-5359-835X
+- family-names: Trappe
+  given-names: Rodja
+  orcid: https://orcid.org/0009-0009-4735-6227
+title: 'NiceGUI: Web-based interfaces with Python. The nice way.'
+version: v1.2.3
+date-released: '2023-03-30'
+url: https://github.com/zauberzeug/nicegui
+doi: 10.5281/zenodo.7785517

+ 2 - 0
main.py

@@ -84,6 +84,8 @@ def add_header() -> None:
         with ui.row().classes('max-lg:hidden'):
             for title, target in menu_items.items():
                 ui.link(title, target).classes(replace='text-lg text-white')
+        with ui.link(target='https://discord.gg/TEpFeAaF4f'):
+            svg.discord().classes('fill-white scale-125 m-1')
         with ui.link(target='https://github.com/zauberzeug/nicegui/'):
             svg.github().classes('fill-white scale-125 m-1')
         add_star()

+ 47 - 1
nicegui/helpers.py

@@ -1,8 +1,12 @@
 import asyncio
 import functools
 import inspect
+import socket
+import threading
+import time
+import webbrowser
 from contextlib import nullcontext
-from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Tuple, Union
 
 from . import background_tasks, globals
 
@@ -33,3 +37,45 @@ def safe_invoke(func: Union[Callable, Awaitable], client: Optional['Client'] = N
                 background_tasks.create(result_with_client())
     except Exception as e:
         globals.handle_exception(e)
+
+
+def is_port_open(host: str, port: int) -> bool:
+    """Check if the port is open by checking if a TCP connection can be established."""
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    try:
+        sock.connect((host, port))
+    except (ConnectionRefusedError, TimeoutError):
+        return False
+    else:
+        return True
+    finally:
+        sock.close()
+
+
+def schedule_browser(host: str, port: int) -> Tuple[threading.Thread, threading.Event]:
+    """Wait non-blockingly for the port to be open, then start a webbrowser.
+
+    This function launches a thread in order to be non-blocking.
+    This thread then uses `is_port_open` to check when the port opens.
+    When connectivity is confirmed, the webbrowser is launched using `webbrowser.open`.
+
+    The thread is created as a daemon thread, in order to not interfere with Ctrl+C.
+
+    If you need to stop this thread, you can do this by setting the Event, that gets returned.
+    The thread will stop with the next loop without opening the browser.
+
+    :return: A tuple consisting of the actual thread object and an event for stopping the thread.
+    """
+    cancel = threading.Event()
+
+    def in_thread(host: str, port: int) -> None:
+        while not is_port_open(host, port):
+            if cancel.is_set():
+                return
+            time.sleep(0.1)
+        webbrowser.open(f'http://{host}:{port}/')
+
+    host = host if host != '0.0.0.0' else '127.0.0.1'
+    thread = threading.Thread(target=in_thread, args=(host, port), daemon=True)
+    thread.start()
+    return thread, cancel

+ 2 - 3
nicegui/run.py

@@ -2,14 +2,13 @@ import logging
 import multiprocessing
 import os
 import sys
-import webbrowser
 from typing import List, Optional, Tuple
 
 import uvicorn
 from uvicorn.main import STARTUP_FAILURE
 from uvicorn.supervisors import ChangeReload, Multiprocess
 
-from . import globals, native_mode
+from . import globals, helpers, native_mode
 
 
 def run(*,
@@ -88,7 +87,7 @@ def run(*,
     os.environ['NICEGUI_URL'] = f'http://{host}:{port}'
 
     if show:
-        webbrowser.open(f'http://{host if host != "0.0.0.0" else "127.0.0.1"}:{port}/')
+        helpers.schedule_browser(host, port)
 
     def split_args(args: str) -> List[str]:
         return [a.strip() for a in args.split(',')]

+ 74 - 0
tests/test_helpers.py

@@ -0,0 +1,74 @@
+import contextlib
+import socket
+import time
+import webbrowser
+
+from nicegui import helpers
+
+
+def test_is_port_open():
+    with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
+        sock.bind(('127.0.0.1', 0))  # port = 0 => the OS chooses a port for us
+        sock.listen(1)
+        host, port = sock.getsockname()
+    assert not helpers.is_port_open(host, port), 'after closing the socket, the port should be free'
+
+    with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
+        sock.bind(('127.0.0.1', port))
+        sock.listen(1)
+        assert helpers.is_port_open(host, port), 'after opening the socket, the port should be detected'
+
+
+def test_schedule_browser(monkeypatch):
+
+    called_with_url = None
+
+    def mock_webbrowser_open(url):
+        nonlocal called_with_url
+        called_with_url = url
+
+    monkeypatch.setattr(webbrowser, 'open', mock_webbrowser_open)
+
+    with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
+
+        sock.bind(('127.0.0.1', 0))
+        host, port = sock.getsockname()
+
+        thread, cancel_event = helpers.schedule_browser(host, port)
+
+        try:
+            # port bound, but not opened yet
+            assert called_with_url is None
+
+            sock.listen()
+            # port opened
+            time.sleep(1)
+            assert called_with_url == f'http://{host}:{port}/'
+        finally:
+            cancel_event.set()
+
+
+def test_canceling_schedule_browser(monkeypatch):
+
+    called_with_url = None
+
+    def mock_webbrowser_open(url):
+        nonlocal called_with_url
+        called_with_url = url
+
+    monkeypatch.setattr(webbrowser, 'open', mock_webbrowser_open)
+
+    # find a free port ...
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.bind(('127.0.0.1', 0))
+    sock.listen(1)
+    host, port = sock.getsockname()
+    # ... and close it so schedule_browser does not launch the browser
+    sock.close()
+
+    thread, cancel_event = helpers.schedule_browser(host, port)
+    time.sleep(0.2)
+    cancel_event.set()
+    time.sleep(0.2)
+    assert not thread.is_alive()
+    assert called_with_url is None

+ 1 - 1
website/demo.py

@@ -27,7 +27,7 @@ class demo:
     def __call__(self, f: Callable) -> Callable:
         with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
             code = inspect.getsource(f).split('# END OF DEMO')[0].strip().splitlines()
-            while not code[0].strip().startswith('def'):
+            while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
                 del code[0]
             del code[0]
             indentation = len(code[0]) - len(code[0].lstrip())

+ 25 - 1
website/more_documentation/icon_documentation.py

@@ -1,5 +1,29 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
-    ui.icon('thumb_up')
+    ui.icon('thumb_up').classes('text-5xl')
+
+
+def more() -> None:
+    ui.add_head_html('<link href="https://unpkg.com/eva-icons@1.1.3/style/eva-icons.css" rel="stylesheet">')
+    ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
+
+    @text_demo('Eva icons', '''
+        You can use [Eva icons](https://akveo.github.io/eva-icons/) in your app.
+    ''')
+    async def eva_icons():
+        # ui.add_head_html('<link href="https://unpkg.com/eva-icons@1.1.3/style/eva-icons.css" rel="stylesheet">')
+
+        ui.element('i').classes('eva eva-github').classes('text-5xl')
+
+    @text_demo('Lottie files', '''
+        You can also use [Lottie files](https://lottiefiles.com/) with animations.
+    ''')
+    async def lottie():
+        # ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
+
+        src = 'https://assets5.lottiefiles.com/packages/lf20_MKCnqtNQvg.json'
+        ui.html(f'<lottie-player src="{src}" loop autoplay />').classes('w-24')

+ 15 - 0
website/more_documentation/image_documentation.py

@@ -1,5 +1,20 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     ui.image('https://picsum.photos/id/377/640/360')
+
+
+def more() -> None:
+    ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
+
+    @text_demo('Lottie files', '''
+        You can also use [Lottie files](https://lottiefiles.com/) with animations.
+    ''')
+    async def lottie():
+        # ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
+
+        src = 'https://assets1.lottiefiles.com/datafiles/HN7OcWNnoqje6iXIiZdWzKxvLIbfeCGTmvXmEm1h/data.json'
+        ui.html(f'<lottie-player src="{src}" loop autoplay />').classes('w-full')

+ 1 - 0
website/static/discord.svg

@@ -0,0 +1 @@
+<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20.317 4.4921C18.7873 3.80147 17.147 3.29265 15.4319 3.00122C15.4007 2.9956 15.3695 3.00965 15.3534 3.03777C15.1424 3.40697 14.9087 3.88862 14.7451 4.26719C12.9004 3.99545 11.0652 3.99545 9.25832 4.26719C9.09465 3.8802 8.85248 3.40697 8.64057 3.03777C8.62449 3.01059 8.59328 2.99654 8.56205 3.00122C6.84791 3.29172 5.20756 3.80054 3.67693 4.4921C3.66368 4.49772 3.65233 4.5071 3.64479 4.51928C0.533392 9.09311 -0.31895 13.5545 0.0991801 17.9606C0.101072 17.9822 0.11337 18.0028 0.130398 18.0159C2.18321 19.4993 4.17171 20.3998 6.12328 20.9967C6.15451 21.0061 6.18761 20.9949 6.20748 20.9695C6.66913 20.3492 7.08064 19.6952 7.43348 19.0073C7.4543 18.967 7.43442 18.9192 7.39186 18.9033C6.73913 18.6597 6.1176 18.3626 5.51973 18.0253C5.47244 17.9981 5.46865 17.9316 5.51216 17.8997C5.63797 17.8069 5.76382 17.7104 5.88396 17.613C5.90569 17.5952 5.93598 17.5914 5.96153 17.6026C9.88928 19.3672 14.1415 19.3672 18.023 17.6026C18.0485 17.5905 18.0788 17.5942 18.1015 17.612C18.2216 17.7095 18.3475 17.8069 18.4742 17.8997C18.5177 17.9316 18.5149 17.9981 18.4676 18.0253C17.8697 18.3692 17.2482 18.6597 16.5945 18.9024C16.552 18.9183 16.533 18.967 16.5538 19.0073C16.9143 19.6942 17.3258 20.3483 17.7789 20.9686C17.7978 20.9949 17.8319 21.0061 17.8631 20.9967C19.8241 20.3998 21.8126 19.4993 23.8654 18.0159C23.8834 18.0028 23.8948 17.9831 23.8967 17.9616C24.3971 12.8676 23.0585 8.4428 20.3482 4.52021C20.3416 4.5071 20.3303 4.49772 20.317 4.4921ZM8.02002 15.2778C6.8375 15.2778 5.86313 14.2095 5.86313 12.8976C5.86313 11.5857 6.8186 10.5175 8.02002 10.5175C9.23087 10.5175 10.1958 11.5951 10.1769 12.8976C10.1769 14.2095 9.22141 15.2778 8.02002 15.2778ZM15.9947 15.2778C14.8123 15.2778 13.8379 14.2095 13.8379 12.8976C13.8379 11.5857 14.7933 10.5175 15.9947 10.5175C17.2056 10.5175 18.1705 11.5951 18.1516 12.8976C18.1516 14.2095 17.2056 15.2778 15.9947 15.2778Z"/></svg>

+ 3 - 0
website/svg.py

@@ -18,3 +18,6 @@ def word() -> ui.html:
 
 def github() -> ui.html:
     return ui.html((PATH / 'github.svg').read_text())
+
+def discord() -> ui.html:
+    return ui.html((PATH / 'discord.svg').read_text())