Skip to content

Commit 6a6ea1a

Browse files
Merge branch 'main' into previous-value
2 parents 3996332 + 09c8b08 commit 6a6ea1a

File tree

11 files changed

+96
-23
lines changed

11 files changed

+96
-23
lines changed

examples/google_oauth2/main.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
from typing import Optional
3+
4+
from authlib.integrations.starlette_client import OAuth, OAuthError
5+
from fastapi import Request
6+
from starlette.responses import RedirectResponse
7+
8+
from nicegui import app, ui
9+
10+
# Get the credentials from the Google Cloud Console
11+
# https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#get_your_google_api_client_id
12+
GOOGLE_CLIENT_ID = '...'
13+
GOOGLE_CLIENT_SECRET = '...'
14+
15+
oauth = OAuth()
16+
oauth.register(
17+
name='google',
18+
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
19+
client_id=GOOGLE_CLIENT_ID,
20+
client_secret=GOOGLE_CLIENT_SECRET,
21+
client_kwargs={'scope': 'openid email profile'},
22+
)
23+
24+
25+
@app.get('/auth')
26+
async def google_oauth(request: Request) -> RedirectResponse:
27+
try:
28+
user_data = await oauth.google.authorize_access_token(request)
29+
except OAuthError as e:
30+
print(f'OAuth error: {e}')
31+
return RedirectResponse('/') # or return an error page/message
32+
app.storage.user['user_data'] = user_data
33+
return RedirectResponse('/')
34+
35+
36+
def logout() -> None:
37+
del app.storage.user['user_data']
38+
ui.navigate.to('/')
39+
40+
41+
@ui.page('/')
42+
async def main(request: Request) -> Optional[RedirectResponse]:
43+
user_data = app.storage.user.get('user_data', None)
44+
if user_data:
45+
ui.label(f'Welcome {user_data.get("userinfo", {}).get("name", "")}!')
46+
ui.button('Logout', on_click=logout)
47+
return None
48+
else:
49+
url = request.url_for('google_oauth')
50+
return await oauth.google.authorize_redirect(request, url)
51+
52+
ui.run(host='localhost', storage_secret='random secret goes here')
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
authlib

examples/google_one_tap_auth/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# For local development, you should add http://localhost:8080 to the authorized JavaScript origins.
1010
# In production, you should add the domain of your website to the authorized JavaScript origins.
1111
# See https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#get_your_google_api_client_id.
12-
GOOGLE_CLIENT_ID = '484798726913-t4es9ner8aglom3miptbnq1m23dsqagi.apps.googleusercontent.com'
12+
GOOGLE_CLIENT_ID = '...'
1313

1414

1515
@ui.page('/')

nicegui/client.py

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(self, page: page, *, request: Optional[Request]) -> None:
5656

5757
self.elements: Dict[int, Element] = {}
5858
self.next_element_id: int = 0
59+
self._waiting_for_connection: asyncio.Event = asyncio.Event()
5960
self.is_waiting_for_connection: bool = False
6061
self.is_waiting_for_disconnect: bool = False
6162
self.environ: Optional[Dict[str, Any]] = None
@@ -177,6 +178,7 @@ def resolve_title(self) -> str:
177178
async def connected(self, timeout: float = 3.0, check_interval: float = 0.1) -> None:
178179
"""Block execution until the client is connected."""
179180
self.is_waiting_for_connection = True
181+
self._waiting_for_connection.set()
180182
deadline = time.time() + timeout
181183
while not self.has_socket_connection:
182184
if time.time() > deadline:

nicegui/elements/json_editor.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ export default {
44
template: "<div></div>",
55
mounted() {
66
this.properties.onChange = (updatedContent, previousContent, { contentErrors, patchResult }) => {
7-
this.$emit("change", { content: updatedContent, errors: contentErrors });
7+
this.$emit("content_change", { content: updatedContent, errors: contentErrors });
88
};
99
this.properties.onSelect = (selection) => {
10-
this.$emit("select", { selection: selection });
10+
this.$emit("content_select", { selection: selection });
1111
};
1212

1313
this.checkValidation();

nicegui/elements/json_editor.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ def on_change(self, callback: Handler[JsonEditorChangeEventArguments]) -> Self:
4848
"""Add a callback to be invoked when the content changes."""
4949
def handle_on_change(e: GenericEventArguments) -> None:
5050
handle_event(callback, JsonEditorChangeEventArguments(sender=self, client=self.client, **e.args))
51-
self.on('change', handle_on_change, ['content', 'errors'])
51+
self.on('content_change', handle_on_change, ['content', 'errors'])
5252
return self
5353

5454
def on_select(self, callback: Handler[JsonEditorSelectEventArguments]) -> Self:
5555
"""Add a callback to be invoked when some of the content has been selected."""
5656
def handle_on_select(e: GenericEventArguments) -> None:
5757
handle_event(callback, JsonEditorSelectEventArguments(sender=self, client=self.client, **e.args))
58-
self.on('select', handle_on_select, ['selection'])
58+
self.on('content_select', handle_on_select, ['selection'])
5959
return self
6060

6161
@property

nicegui/elements/notification.py

+13
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,19 @@ def spinner(self, value: bool) -> None:
177177
self._props['options']['spinner'] = value
178178
self.update()
179179

180+
@property
181+
def timeout(self) -> float:
182+
"""Timeout of the notification in seconds.
183+
184+
*Added in version 2.13.0*
185+
"""
186+
return self._props['options']['timeout'] / 1000
187+
188+
@timeout.setter
189+
def timeout(self, value: Optional[float]) -> None:
190+
self._props['options']['timeout'] = (value or 0) * 1000
191+
self.update()
192+
180193
@property
181194
def close_button(self) -> Union[bool, str]:
182195
"""Whether the notification has a close button."""

nicegui/page.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import asyncio
44
import inspect
5-
import time
65
from functools import wraps
76
from pathlib import Path
87
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
@@ -124,11 +123,13 @@ async def wait_for_result() -> None:
124123
with client:
125124
return await result
126125
task = background_tasks.create(wait_for_result())
127-
deadline = time.time() + self.response_timeout
128-
while task and not client.is_waiting_for_connection and not task.done():
129-
if time.time() > deadline:
130-
raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
131-
await asyncio.sleep(0.1)
126+
try:
127+
await asyncio.wait([
128+
task,
129+
asyncio.create_task(client._waiting_for_connection.wait()), # pylint: disable=protected-access
130+
], timeout=self.response_timeout, return_when=asyncio.FIRST_COMPLETED)
131+
except asyncio.TimeoutError as e:
132+
raise TimeoutError(f'Response not ready after {self.response_timeout} seconds') from e
132133
if task.done():
133134
result = task.result()
134135
else:

tests/test_dialog.py

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from typing import List
2-
31
from selenium.webdriver.common.keys import Keys
42

53
from nicegui import ui
@@ -24,22 +22,23 @@ def test_open_close_dialog(screen: Screen):
2422

2523
def test_await_dialog(screen: Screen):
2624
with ui.dialog() as dialog, ui.card():
27-
ui.label('Are you sure?')
28-
with ui.row():
29-
ui.button('Yes', on_click=lambda: dialog.submit('Yes'))
30-
ui.button('No', on_click=lambda: dialog.submit('No'))
25+
ui.button('Yes', on_click=lambda: dialog.submit('Yes'))
26+
ui.button('No', on_click=lambda: dialog.submit('No'))
3127

3228
async def show() -> None:
33-
results.append(await dialog)
34-
results: List[str] = []
29+
ui.notify(f'Result: {await dialog}')
30+
3531
ui.button('Open', on_click=show)
3632

3733
screen.open('/')
3834
screen.click('Open')
3935
screen.click('Yes')
36+
screen.should_contain('Result: Yes')
37+
4038
screen.click('Open')
4139
screen.click('No')
40+
screen.should_contain('Result: No')
41+
4242
screen.click('Open')
4343
screen.type(Keys.ESCAPE)
44-
screen.wait(0.5)
45-
assert results == ['Yes', 'No', None]
44+
screen.should_contain('Result: None')

tests/test_teleport.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,15 @@ def create_teleport():
4747
def rebuild_card():
4848
card.delete()
4949
ui.card().classes('card')
50-
teleport.update() # type: ignore
50+
assert teleport is not None
51+
teleport.update()
52+
ui.notify('Card rebuilt')
5153

5254
ui.button('rebuild card', on_click=rebuild_card)
5355

5456
screen.open('/')
5557
screen.click('create')
5658
screen.should_contain('Hello')
5759
screen.click('rebuild card')
60+
screen.should_contain('Card rebuilt')
5861
assert screen.find_by_css('.card > div').text == 'Hello'

website/examples.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Example:
1313

1414
def __post_init__(self) -> None:
1515
"""Post-initialization hook."""
16-
name = self.title.lower().replace(' ', '_')
16+
name = self.title.lower().replace(' ', '_').replace('-', '_')
1717
content = [p for p in (PATH / name).glob('*') if not p.name.startswith(('__pycache__', '.', 'test_'))]
1818
filename = 'main.py' if len(content) == 1 else ''
1919
self.url = f'https://github.com/zauberzeug/nicegui/tree/main/examples/{name}/{filename}'
@@ -71,4 +71,6 @@ def __post_init__(self) -> None:
7171
Example('Signature Pad', 'A custom element based on [signature_pad](https://www.npmjs.com/package/signature_pad'),
7272
Example('OpenAI Assistant', "Using OpenAI's Assistant API with async/await"),
7373
Example('Redis Storage', 'Use Redis storage to share data across multiple instances behind a reverse proxy or load balancer'),
74+
Example('Google One-Tap Auth', 'Authenticate users via Google One-Tap'),
75+
Example('Google OAuth2', 'Authenticate with Google OAuth2')
7476
]

0 commit comments

Comments
 (0)