From 9f9cf619c152e11f3734561bf291a338aecc6fce Mon Sep 17 00:00:00 2001 From: Larry Ogrodnek Date: Mon, 12 Aug 2024 21:52:52 -0400 Subject: [PATCH] clean up examples a bit (#33) --- examples/app.py | 140 +++--------------- examples/format_examples.py | 35 +++++ examples/views/__init__.py | 5 +- examples/views/count/__init__.py | 1 + examples/views/{ => count}/count.html | 0 examples/views/{ => count}/count.py | 7 + examples/views/count_pubsub/__init__.py | 1 + .../{ => count_pubsub}/count_pubsub.html | 0 .../views/{ => count_pubsub}/count_pubsub.py | 7 + examples/views/fifa/fifa.py | 10 +- examples/views/file_upload/file_upload.py | 7 +- examples/views/form_validation/plants.py | 8 +- examples/views/js_commands/js_commands.py | 6 + examples/views/kanban/kanban.py | 7 + examples/views/maps/map.py | 7 + examples/views/podcasts/podcasts.py | 6 + examples/views/presence/presence.py | 7 +- examples/views/registration/registration.py | 18 ++- examples/views/status/status.py | 6 + examples/views/volume/__init__.py | 1 + examples/views/{ => volume}/volume.html | 0 examples/views/{ => volume}/volume.py | 6 + examples/views/webping/webping.py | 10 +- 23 files changed, 167 insertions(+), 128 deletions(-) create mode 100644 examples/format_examples.py create mode 100644 examples/views/count/__init__.py rename examples/views/{ => count}/count.html (100%) rename examples/views/{ => count}/count.py (80%) create mode 100644 examples/views/count_pubsub/__init__.py rename examples/views/{ => count_pubsub}/count_pubsub.html (100%) rename examples/views/{ => count_pubsub}/count_pubsub.py (85%) create mode 100644 examples/views/volume/__init__.py rename examples/views/{ => volume}/volume.html (100%) rename examples/views/{ => volume}/volume.py (94%) diff --git a/examples/app.py b/examples/app.py index c4f996a..8a1013b 100644 --- a/examples/app.py +++ b/examples/app.py @@ -3,6 +3,7 @@ from starlette.routing import Route from pyview import PyView, defaultRootTemplate from markupsafe import Markup +from .format_examples import ExampleEntry, format_examples from .views import ( CountLiveView, @@ -62,76 +63,16 @@ def content_wrapper(_context, content: Markup) -> Markup: app.rootTemplate = defaultRootTemplate(css=Markup(css), content_wrapper=content_wrapper) routes = [ - ( - "/count", - CountLiveView, - "Basic Counter", - "count.py", - """ - Gotta start somewhere, right? This example shows how to send click events - to the backend to update state. We also snuck in handling URL params. - """, - ), - ( - "/count_pubsub", - CountLiveViewPubSub, - "Basic Counter with PubSub", - "count_pubsub.py", - """ - The counter example, but with PubSub. Open this example in multiple windows - to see the state update in real time across all windows. - """, - ), - ("/volume", VolumeLiveView, "Volume Control", "volume.py", "Keyboard events!"), - ( - "/registration", - RegistrationLiveView, - "Registration Form Validation", - "registration", - "Form validation using Pydantic", - ), - ("/plants", PlantsLiveView, "Form Validation 2", "form_validation", ""), - ( - "/fifa", - FifaAudienceLiveView, - "Table Pagination", - "fifa", - "Table Pagination, and updating the URL from the backend.", - ), - ( - "/podcasts", - PodcastLiveView, - "Podcasts", - "podcasts", - """ - URL Parameters, client navigation updates, and dynamic page titles. - """, - ), - ( - "/status", - StatusLiveView, - "Realtime Status Dashboard", - "status", - "Pushing updates from the backend to the client.", - ), - ( - "/js_commands", - JsCommandsLiveView, - "JS Commands", - "js_commands", - """ - JS Commands let you update the DOM without making a trip to the server. - """, - ), - ( - "/webping", - PingLiveView, - "Web Ping", - "webping", - """ - Another example of pushing updates from the backend to the client. - """, - ), + ("/count", CountLiveView), + ("/count_pubsub", CountLiveViewPubSub), + ("/volume", VolumeLiveView), + ("/registration", RegistrationLiveView), + ("/plants", PlantsLiveView), + ("/fifa", FifaAudienceLiveView), + ("/podcasts", PodcastLiveView), + ("/status", StatusLiveView), + ("/js_commands", JsCommandsLiveView), + ("/webping", PingLiveView), # ( # "/checkboxes", # CheckboxLiveView, @@ -141,58 +82,20 @@ def content_wrapper(_context, content: Markup) -> Markup: # A silly multi-user game where you can click checkboxes. # """, # ), - ( - "/presence", - PresenceLiveView, - "Presence", - "presence", - """ - A simple example of presence tracking. Open this example in multiple windows - """, - ), - ( - "/maps", - MapLiveView, - "Maps", - "maps", - """ - A simple example of using Leaflet.js with PyView, and sending information back and - forth between the liveview and the JS library. - """, - ), - ( - "/file_upload", - FileUploadDemoLiveView, - "File Upload", - "file_upload", - """ - File upload example, with previews and progress bars. - """, - ), - ( - "/kanban", - KanbanLiveView, - "Kanban Board", - "kanban", - """ - A simple Kanban board example with drag and drop (another hooks example showing integration w/ SortableJS). - """, - ), + ("/presence", PresenceLiveView), + ("/maps", MapLiveView), + ("/file_upload", FileUploadDemoLiveView), + ("/kanban", KanbanLiveView), ] -for path, view, _, _, _ in routes: - app.add_live_view(path, view) - async def get(request): - def render_example(path, title, src_file, text): - src_link = ( - f"https://github.com/ogrodnek/pyview/tree/main/examples/views/{src_file}" - ) + def render_example(e: ExampleEntry): + src_link = f"https://github.com/ogrodnek/pyview/tree/main/{e.src_path}" return f"""
-

{title}

-

{text}

+

{e.title}

+

{e.text}

Source Code

""" @@ -211,7 +114,7 @@ def render_example(path, title, src_file, text):

PyView Examples

- {"".join([render_example(k,t,src, text) for k, _, t, src, text in routes])} + {"".join([render_example(e) for e in format_examples(routes)])}
@@ -220,3 +123,6 @@ def render_example(path, title, src_file, text): app.routes.append(Route("/", get, methods=["GET"])) + +for path, view in routes: + app.add_live_view(path, view) diff --git a/examples/format_examples.py b/examples/format_examples.py new file mode 100644 index 0000000..0d6e648 --- /dev/null +++ b/examples/format_examples.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from pyview import LiveView +from typing import Optional, Iterator + + +@dataclass +class ExampleEntry: + url_path: str + title: str + src_path: str + text: str + + +def format_example(url_path: str, lv: type[LiveView]) -> Optional[ExampleEntry]: + if not lv.__doc__: + return None + + # parse name and title from docstring, separated by blank line + docs = lv.__doc__.strip().split("\n\n") + title = docs[0] + text = "".join(docs[1:]) + + # get dirpectory path from module name + src_path = "/".join(lv.__module__.split(".")[:-1]) + + return ExampleEntry(url_path, title, src_path, text.strip()) + + +def format_examples( + routes: list[tuple[str, type[LiveView]]], +) -> Iterator[ExampleEntry]: + for url, lv in routes: + f = format_example(url, lv) + if f: + yield f diff --git a/examples/views/__init__.py b/examples/views/__init__.py index 39bc0fe..2181521 100644 --- a/examples/views/__init__.py +++ b/examples/views/__init__.py @@ -1,11 +1,9 @@ -from .count import CountLiveView from .volume import VolumeLiveView from .fifa import FifaAudienceLiveView from .status import StatusLiveView from .podcasts import PodcastLiveView from .form_validation import PlantsLiveView from .registration import RegistrationLiveView -from .count_pubsub import CountLiveViewPubSub from .js_commands import JsCommandsLiveView from .webping import PingLiveView from .checkboxes import CheckboxLiveView @@ -13,9 +11,10 @@ from .maps import MapLiveView from .file_upload import FileUploadDemoLiveView from .kanban import KanbanLiveView +from .count_pubsub import CountLiveViewPubSub +from .count import CountLiveView __all__ = [ - "CountLiveView", "CountLiveView", "VolumeLiveView", "FifaAudienceLiveView", diff --git a/examples/views/count/__init__.py b/examples/views/count/__init__.py new file mode 100644 index 0000000..61c30e0 --- /dev/null +++ b/examples/views/count/__init__.py @@ -0,0 +1 @@ +from .count import CountLiveView diff --git a/examples/views/count.html b/examples/views/count/count.html similarity index 100% rename from examples/views/count.html rename to examples/views/count/count.html diff --git a/examples/views/count.py b/examples/views/count/count.py similarity index 80% rename from examples/views/count.py rename to examples/views/count/count.py index 303f2f4..ab3f199 100644 --- a/examples/views/count.py +++ b/examples/views/count/count.py @@ -7,6 +7,13 @@ class CountContext(TypedDict): class CountLiveView(LiveView[CountContext]): + """ + Basic Counter + + Gotta start somewhere, right? This example shows how to send click events + to the backend to update state. We also snuck in handling URL params. + """ + async def mount(self, socket: LiveViewSocket[CountContext], _session): socket.context = {"count": 0} diff --git a/examples/views/count_pubsub/__init__.py b/examples/views/count_pubsub/__init__.py new file mode 100644 index 0000000..c1a0f59 --- /dev/null +++ b/examples/views/count_pubsub/__init__.py @@ -0,0 +1 @@ +from .count_pubsub import CountLiveViewPubSub diff --git a/examples/views/count_pubsub.html b/examples/views/count_pubsub/count_pubsub.html similarity index 100% rename from examples/views/count_pubsub.html rename to examples/views/count_pubsub/count_pubsub.html diff --git a/examples/views/count_pubsub.py b/examples/views/count_pubsub/count_pubsub.py similarity index 85% rename from examples/views/count_pubsub.py rename to examples/views/count_pubsub/count_pubsub.py index f472cf9..cd62e6d 100644 --- a/examples/views/count_pubsub.py +++ b/examples/views/count_pubsub/count_pubsub.py @@ -14,6 +14,13 @@ def increment(self): class CountLiveViewPubSub(LiveView[Count]): + """ + Basic Counter with PubSub + + The counter example, but with PubSub. Open this example in multiple windows + to see the state update in real time across all windows. + """ + async def mount(self, socket: LiveViewSocket[Count], _session): socket.context = Count() if socket.connected: diff --git a/examples/views/fifa/fifa.py b/examples/views/fifa/fifa.py index d159e00..d16b491 100644 --- a/examples/views/fifa/fifa.py +++ b/examples/views/fifa/fifa.py @@ -9,6 +9,12 @@ class FifaContext(TypedDict): class FifaAudienceLiveView(LiveView[FifaContext]): + """ + Table Pagination + + Table Pagination, and updating the URL from the backend. + """ + async def mount(self, socket: LiveViewSocket[FifaContext], _session): paging = Paging(1, 10) audiences = list_items(paging) @@ -22,7 +28,9 @@ async def handle_event(self, event, payload, socket: LiveViewSocket[FifaContext] socket.context["audiences"] = audiences - await socket.push_patch("/fifa", {"page": paging.page, "perPage": paging.perPage}) + await socket.push_patch( + "/fifa", {"page": paging.page, "perPage": paging.perPage} + ) async def handle_params(self, url, params, socket: LiveViewSocket[FifaContext]): paging = socket.context["paging"] diff --git a/examples/views/file_upload/file_upload.py b/examples/views/file_upload/file_upload.py index 19a5442..168bc91 100644 --- a/examples/views/file_upload/file_upload.py +++ b/examples/views/file_upload/file_upload.py @@ -1,6 +1,5 @@ from pyview import LiveView, LiveViewSocket from pyview.uploads import UploadConfig, UploadConstraints -from typing import Optional from dataclasses import dataclass, field from pyview.vendor.ibis import filters from .file_repository import FileRepository, FileEntry @@ -29,6 +28,12 @@ class FileUploadDemoContext: class FileUploadDemoLiveView(LiveView[FileUploadDemoContext]): + """ + File Upload + + File upload example, with previews and progress bars. + """ + async def mount(self, socket: LiveViewSocket[FileUploadDemoContext], _session): config = socket.allow_upload( "photos", diff --git a/examples/views/form_validation/plants.py b/examples/views/form_validation/plants.py index 01e2202..6fbdc96 100644 --- a/examples/views/form_validation/plants.py +++ b/examples/views/form_validation/plants.py @@ -18,12 +18,18 @@ class Config: class PlantsLiveView(LiveView[PlantsContext]): + """ + More Form Validation + """ + async def mount(self, socket: LiveViewSocket[PlantsContext], _session): socket.context = PlantsContext(plants()) async def handle_event(self, event, payload, socket: LiveViewSocket[PlantsContext]): if event == "water": - plant = next((p for p in socket.context.plants if p.id == payload["id"]), None) + plant = next( + (p for p in socket.context.plants if p.id == payload["id"]), None + ) if plant: print(f"Watering {plant.name}...") plant.last_watered = datetime.now() diff --git a/examples/views/js_commands/js_commands.py b/examples/views/js_commands/js_commands.py index c666dc4..6f199f8 100644 --- a/examples/views/js_commands/js_commands.py +++ b/examples/views/js_commands/js_commands.py @@ -2,5 +2,11 @@ class JsCommandsLiveView(LiveView[dict]): + """ + JS Commands + + JS Commands let you update the DOM without making a trip to the server. + """ + async def mount(self, socket: LiveViewSocket[dict], _session): socket.context = {} diff --git a/examples/views/kanban/kanban.py b/examples/views/kanban/kanban.py index 2a0037c..7753b98 100644 --- a/examples/views/kanban/kanban.py +++ b/examples/views/kanban/kanban.py @@ -25,6 +25,13 @@ def __post_init__(self): class KanbanLiveView(LiveView[KanbanContext]): + """ + Kanban Board + + A simple Kanban board example with drag and drop + (another hooks example showing integration w/ SortableJS). + """ + async def mount(self, socket: LiveViewSocket[KanbanContext], _session): socket.context = KanbanContext() diff --git a/examples/views/maps/map.py b/examples/views/maps/map.py index 06f8daa..425d49b 100644 --- a/examples/views/maps/map.py +++ b/examples/views/maps/map.py @@ -18,6 +18,13 @@ class MapContext: class MapLiveView(LiveView[MapContext]): + """ + Maps + + A simple example of using Leaflet.js with PyView, and sending information back and + forth between the liveview and the JS library. + """ + async def mount(self, socket: LiveViewSocket[MapContext], _session): socket.context = MapContext( parks=national_parks, selected_park_name=national_parks[0]["name"] diff --git a/examples/views/podcasts/podcasts.py b/examples/views/podcasts/podcasts.py index 33050dd..c0cfb7a 100644 --- a/examples/views/podcasts/podcasts.py +++ b/examples/views/podcasts/podcasts.py @@ -12,6 +12,12 @@ class PodcastContext: class PodcastLiveView(LiveView): + """ + Podcasts + + URL Parameters, client navigation updates, and dynamic page titles. + """ + async def mount(self, socket: LiveViewSocket[PodcastContext], _session): casts = podcasts() socket.context = PodcastContext(casts[0], casts) diff --git a/examples/views/presence/presence.py b/examples/views/presence/presence.py index 79bcd9d..2f4a4fb 100644 --- a/examples/views/presence/presence.py +++ b/examples/views/presence/presence.py @@ -23,8 +23,13 @@ class PresenceContext: class PresenceLiveView(LiveView[PresenceContext]): - async def mount(self, socket: LiveViewSocket[PresenceContext], _session): + """ + Presence + + A simple example of presence tracking. Open this example in multiple windows. + """ + async def mount(self, socket: LiveViewSocket[PresenceContext], _session): socket.context = PresenceContext(connected=USER_REPO.all()) if socket.connected: diff --git a/examples/views/registration/registration.py b/examples/views/registration/registration.py index 2675476..d648e66 100644 --- a/examples/views/registration/registration.py +++ b/examples/views/registration/registration.py @@ -9,11 +9,15 @@ @filters.register -def input_tag(changeset: ChangeSet, field_name: str, options: Optional[dict[str, str]] = None) -> Markup: +def input_tag( + changeset: ChangeSet, field_name: str, options: Optional[dict[str, str]] = None +) -> Markup: type = (options or {}).get("type", "text") return Markup( """""" - ).format(type=type, field_name=field_name, value=changeset.changes.get(field_name, "")) + ).format( + type=type, field_name=field_name, value=changeset.changes.get(field_name, "") + ) @filters.register @@ -44,10 +48,18 @@ class RegistrationContext(TypedDict): class RegistrationLiveView(LiveView): + """ + Registration Form Validation + + Form validation using Pydantic + """ + async def mount(self, socket: LiveViewSocket, _session): socket.context = RegistrationContext(changeset=change_set(Registration)) - async def handle_event(self, event, payload, socket: LiveViewSocket[RegistrationContext]): + async def handle_event( + self, event, payload, socket: LiveViewSocket[RegistrationContext] + ): print(event, payload) if event == "validate": socket.context["changeset"].apply(payload) diff --git a/examples/views/status/status.py b/examples/views/status/status.py index f093379..a22c11d 100644 --- a/examples/views/status/status.py +++ b/examples/views/status/status.py @@ -38,6 +38,12 @@ def mem(self) -> str: class StatusLiveView(LiveView[StatusContext]): + """ + Realtime Status Dashboard + + Pushing updates from the backend to the client. + """ + async def mount(self, socket: LiveViewSocket[StatusContext], _session): socket.context = StatusContext() if socket.connected: diff --git a/examples/views/volume/__init__.py b/examples/views/volume/__init__.py new file mode 100644 index 0000000..3941444 --- /dev/null +++ b/examples/views/volume/__init__.py @@ -0,0 +1 @@ +from .volume import VolumeLiveView diff --git a/examples/views/volume.html b/examples/views/volume/volume.html similarity index 100% rename from examples/views/volume.html rename to examples/views/volume/volume.html diff --git a/examples/views/volume.py b/examples/views/volume/volume.py similarity index 94% rename from examples/views/volume.py rename to examples/views/volume/volume.py index d4418dd..0c23363 100644 --- a/examples/views/volume.py +++ b/examples/views/volume/volume.py @@ -8,6 +8,12 @@ class Volume: class VolumeLiveView(LiveView[Volume]): + """ + Volume Control + + Keyboard events! + """ + async def mount(self, socket: LiveViewSocket[Volume], _session): socket.context = Volume() diff --git a/examples/views/webping/webping.py b/examples/views/webping/webping.py index 0c728dd..0d75454 100644 --- a/examples/views/webping/webping.py +++ b/examples/views/webping/webping.py @@ -39,6 +39,12 @@ class PingContext: class PingLiveView(LiveView[PingContext]): + """ + Web Ping + + Another example of pushing updates from the backend to the client. + """ + async def mount(self, socket: LiveViewSocket[PingContext], _session): socket.context = PingContext( [ @@ -59,7 +65,9 @@ async def ping(self, site: PingSite): async with session.head(site.url) as response: status = response.status diff = (time.time_ns() - start) / 1_000_000 - site.responses.append(PingResponse(status, diff, datetime.datetime.now())) + site.responses.append( + PingResponse(status, diff, datetime.datetime.now()) + ) site.status = "OK" if status == 200 else "Error" async def handle_info(self, event, socket: LiveViewSocket[PingContext]):