Bladeren bron

isolate some functions from helpers.py

Falko Schindler 1 jaar geleden
bovenliggende
commit
d7ea66c035
7 gewijzigde bestanden met toevoegingen van 96 en 94 verwijderingen
  1. 2 0
      nicegui/__init__.py
  2. 7 0
      nicegui/app/__init__.py
  3. 12 11
      nicegui/app/app.py
  4. 2 2
      nicegui/app/app_config.py
  5. 45 0
      nicegui/app/streaming_response.py
  6. 27 8
      nicegui/client.py
  7. 1 73
      nicegui/helpers.py

+ 2 - 0
nicegui/__init__.py

@@ -1,5 +1,6 @@
 from . import context, elements, run, ui
 from .api_router import APIRouter
+from .app.app import App
 from .awaitable_response import AwaitableResponse
 from .client import Client
 from .nicegui import app
@@ -9,6 +10,7 @@ from .version import __version__
 __all__ = [
     'APIRouter',
     'app',
+    'App',
     'AwaitableResponse',
     'Client',
     'context',

+ 7 - 0
nicegui/app/__init__.py

@@ -0,0 +1,7 @@
+from .app import App
+from .app_config import AppConfig
+
+__all__ = [
+    'App',
+    'AppConfig',
+]

+ 12 - 11
nicegui/app.py → nicegui/app/app.py

@@ -7,14 +7,15 @@ from fastapi import FastAPI, HTTPException, Request
 from fastapi.responses import FileResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 
-from . import background_tasks, helpers
+from .. import background_tasks, helpers
+from ..client import Client
+from ..logging import log
+from ..native import NativeConfig
+from ..observables import ObservableSet
+from ..server import Server
+from ..storage import Storage
 from .app_config import AppConfig
-from .client import Client
-from .logging import log
-from .native import NativeConfig
-from .observables import ObservableSet
-from .server import Server
-from .storage import Storage
+from .streaming_response import get_streaming_response
 
 
 class State(Enum):
@@ -64,14 +65,14 @@ class App(FastAPI):
         """Start NiceGUI. (For internal use only.)"""
         self._state = State.STARTING
         for t in self._startup_handlers:
-            helpers.safe_invoke(t, Client.auto_index_client)
+            Client.auto_index_client.safe_invoke(t)
         self._state = State.STARTED
 
     def stop(self) -> None:
         """Stop NiceGUI. (For internal use only.)"""
         self._state = State.STOPPING
         for t in self._shutdown_handlers:
-            helpers.safe_invoke(t, Client.auto_index_client)
+            Client.auto_index_client.safe_invoke(t)
         self._state = State.STOPPED
 
     def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
@@ -199,7 +200,7 @@ class App(FastAPI):
             filepath = Path(local_directory) / filename
             if not filepath.is_file():
                 raise HTTPException(status_code=404, detail='Not Found')
-            return helpers.get_streaming_response(filepath, request)
+            return get_streaming_response(filepath, request)
 
     def add_media_file(self, *,
                        local_file: Union[str, Path],
@@ -228,7 +229,7 @@ class App(FastAPI):
         def read_item(request: Request) -> StreamingResponse:
             if single_use:
                 self.remove_route(path)
-            return helpers.get_streaming_response(file, request)
+            return get_streaming_response(file, request)
 
         return path
 

+ 2 - 2
nicegui/app_config.py → nicegui/app/app_config.py

@@ -2,8 +2,8 @@ from dataclasses import dataclass, field
 from pathlib import Path
 from typing import Dict, List, Literal, Optional, Union
 
-from .dataclasses import KWONLY_SLOTS
-from .language import Language
+from ..dataclasses import KWONLY_SLOTS
+from ..language import Language
 
 
 @dataclass(**KWONLY_SLOTS)

+ 45 - 0
nicegui/app/streaming_response.py

@@ -0,0 +1,45 @@
+import mimetypes
+from pathlib import Path
+from typing import Generator
+
+from fastapi import Request
+from fastapi.responses import StreamingResponse
+
+mimetypes.init()
+
+
+def get_streaming_response(file: Path, request: Request) -> StreamingResponse:
+    """Get a StreamingResponse for the given file and request."""
+    file_size = file.stat().st_size
+    start = 0
+    end = file_size - 1
+    range_header = request.headers.get('Range')
+    if range_header:
+        byte1, byte2 = range_header.split('=')[1].split('-')
+        start = int(byte1)
+        if byte2:
+            end = int(byte2)
+    content_length = end - start + 1
+    headers = {
+        'Content-Range': f'bytes {start}-{end}/{file_size}',
+        'Content-Length': str(content_length),
+        'Accept-Ranges': 'bytes',
+    }
+
+    def content_reader(file: Path, start: int, end: int, chunk_size: int = 8192) -> Generator[bytes, None, None]:
+        with open(file, 'rb') as data:
+            data.seek(start)
+            remaining_bytes = end - start + 1
+            while remaining_bytes > 0:
+                chunk = data.read(min(chunk_size, remaining_bytes))
+                if not chunk:
+                    break
+                yield chunk
+                remaining_bytes -= len(chunk)
+
+    return StreamingResponse(
+        content_reader(file, start, end),
+        media_type=mimetypes.guess_type(str(file))[0] or 'application/octet-stream',
+        headers=headers,
+        status_code=206,
+    )

+ 27 - 8
nicegui/client.py

@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import asyncio
+import inspect
 import time
 import uuid
 from contextlib import contextmanager
@@ -13,12 +14,11 @@ from fastapi.templating import Jinja2Templates
 
 from nicegui import json
 
-from . import background_tasks, binding, core, outbox
+from . import background_tasks, binding, core, helpers, outbox
 from .awaitable_response import AwaitableResponse
 from .dependencies import generate_resources
 from .element import Element
 from .favicon import get_favicon_url
-from .helpers import safe_invoke
 from .logging import log
 from .version import __version__
 
@@ -201,9 +201,9 @@ class Client:
             self._disconnect_task.cancel()
             self._disconnect_task = None
         for t in self.connect_handlers:
-            safe_invoke(t, self)
+            self.safe_invoke(t)
         for t in core.app._connect_handlers:  # pylint: disable=protected-access
-            safe_invoke(t, self)
+            self.safe_invoke(t)
 
     def handle_disconnect(self) -> None:
         """Wait for the browser to reconnect; invoke disconnect handlers if it doesn't."""
@@ -213,12 +213,12 @@ class Client:
             else:
                 delay = core.app.config.reconnect_timeout  # pylint: disable=protected-access
             await asyncio.sleep(delay)
-            if not self.shared:
-                self.delete()
             for t in self.disconnect_handlers:
-                safe_invoke(t, self)
+                self.safe_invoke(t)
             for t in core.app._disconnect_handlers:  # pylint: disable=protected-access
-                safe_invoke(t, self)
+                self.safe_invoke(t)
+            if not self.shared:
+                self.delete()
         self._disconnect_task = background_tasks.create(handle_disconnect())
 
     def handle_event(self, msg: Dict) -> None:
@@ -235,6 +235,25 @@ class Client:
         """Store the result of a JavaScript command."""
         self.waiting_javascript_commands[msg['request_id']] = msg['result']
 
+    def safe_invoke(self, func: Union[Callable[..., Any], Awaitable]) -> None:
+        """Invoke the potentially async function in the client context and catch any exceptions."""
+        try:
+            if isinstance(func, Awaitable):
+                async def func_with_client():
+                    with self:
+                        await func
+                background_tasks.create(func_with_client())
+            else:
+                with self:
+                    result = func(self) if len(inspect.signature(func).parameters) == 1 else func()
+                if helpers.is_coroutine_function(func):
+                    async def result_with_client():
+                        with self:
+                            await result
+                    background_tasks.create(result_with_client())
+        except Exception as e:
+            core.app.handle_exception(e)
+
     def remove_elements(self, elements: Iterable[Element]) -> None:
         """Remove the given elements from the client."""
         binding.remove(elements, Element)

+ 1 - 73
nicegui/helpers.py

@@ -1,28 +1,13 @@
-from __future__ import annotations
-
 import asyncio
 import functools
 import hashlib
-import inspect
-import mimetypes
 import socket
 import sys
 import threading
 import time
 import webbrowser
-from contextlib import nullcontext
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Awaitable, Callable, Generator, Optional, Tuple, Union
-
-from fastapi import Request
-from fastapi.responses import StreamingResponse
-
-from . import background_tasks, core
-
-if TYPE_CHECKING:
-    from .client import Client
-
-mimetypes.init()
+from typing import Any, Optional, Tuple, Union
 
 
 def is_pytest() -> bool:
@@ -58,26 +43,6 @@ def hash_file_path(path: Path) -> str:
     return hashlib.sha256(path.as_posix().encode()).hexdigest()[:32]
 
 
-def safe_invoke(func: Union[Callable[..., Any], Awaitable], client: Optional[Client] = None) -> None:
-    """Invoke the potentially async function in the client context and catch any exceptions."""
-    try:
-        if isinstance(func, Awaitable):
-            async def func_with_client():
-                with client or nullcontext():
-                    await func
-            background_tasks.create(func_with_client())
-        else:
-            with client or nullcontext():
-                result = func(client) if len(inspect.signature(func).parameters) == 1 and client is not None else func()
-            if is_coroutine_function(func):
-                async def result_with_client():
-                    with client or nullcontext():
-                        await result
-                background_tasks.create(result_with_client())
-    except Exception as e:
-        core.app.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)
@@ -120,40 +85,3 @@ def schedule_browser(host: str, port: int) -> Tuple[threading.Thread, threading.
     thread = threading.Thread(target=in_thread, args=(host, port), daemon=True)
     thread.start()
     return thread, cancel
-
-
-def get_streaming_response(file: Path, request: Request) -> StreamingResponse:
-    """Get a StreamingResponse for the given file and request."""
-    file_size = file.stat().st_size
-    start = 0
-    end = file_size - 1
-    range_header = request.headers.get('Range')
-    if range_header:
-        byte1, byte2 = range_header.split('=')[1].split('-')
-        start = int(byte1)
-        if byte2:
-            end = int(byte2)
-    content_length = end - start + 1
-    headers = {
-        'Content-Range': f'bytes {start}-{end}/{file_size}',
-        'Content-Length': str(content_length),
-        'Accept-Ranges': 'bytes',
-    }
-
-    def content_reader(file: Path, start: int, end: int, chunk_size: int = 8192) -> Generator[bytes, None, None]:
-        with open(file, 'rb') as data:
-            data.seek(start)
-            remaining_bytes = end - start + 1
-            while remaining_bytes > 0:
-                chunk = data.read(min(chunk_size, remaining_bytes))
-                if not chunk:
-                    break
-                yield chunk
-                remaining_bytes -= len(chunk)
-
-    return StreamingResponse(
-        content_reader(file, start, end),
-        media_type=mimetypes.guess_type(str(file))[0] or 'application/octet-stream',
-        headers=headers,
-        status_code=206,
-    )