From 16fa3cdd90be091aa32ef150bf96c26575bbda26 Mon Sep 17 00:00:00 2001 From: Chase Sterling Date: Fri, 11 Apr 2025 15:01:09 -0400 Subject: [PATCH 1/2] Python SDK: change `merge_fragments` to take a string rather than list (#822) * python sdk: change merge_fragments to take a string rather than list * Change merge_fragments for fasthtml to accept a string rather than list --- examples/python/django/ds/views.py | 4 ++-- examples/python/fastapi/app.py | 2 +- examples/python/fasthtml/advanced.py | 8 ++++---- examples/python/fasthtml/simple.py | 2 +- examples/python/quart/app.py | 2 +- examples/python/sanic/app.py | 18 ++++++------------ sdk/python/src/datastar_py/fasthtml.py | 5 +++-- sdk/python/src/datastar_py/sse.py | 5 ++--- 8 files changed, 20 insertions(+), 26 deletions(-) diff --git a/examples/python/django/ds/views.py b/examples/python/django/ds/views.py index 6cb2da03..470aebf3 100644 --- a/examples/python/django/ds/views.py +++ b/examples/python/django/ds/views.py @@ -52,7 +52,7 @@ async def updates_asgi(request): async def time_updates(): while True: yield DatastarStreamingHttpResponse.merge_fragments( - [f"""{datetime.now().isoformat()}"""] + f"""{datetime.now().isoformat()}""" ) await asyncio.sleep(1) yield DatastarStreamingHttpResponse.merge_signals( @@ -110,7 +110,7 @@ def updates_wsgi(request): def time_updates(): while True: yield DatastarStreamingHttpResponse.merge_fragments( - [f"""{datetime.now().isoformat()}"""] + f"""{datetime.now().isoformat()}""" ) time.sleep(0.5) yield DatastarStreamingHttpResponse.merge_signals( diff --git a/examples/python/fastapi/app.py b/examples/python/fastapi/app.py index 6c904801..0b84bb0a 100644 --- a/examples/python/fastapi/app.py +++ b/examples/python/fastapi/app.py @@ -54,7 +54,7 @@ async def read_root(): async def time_updates(): while True: yield DatastarStreamingResponse.merge_fragments( - [f"""{datetime.now().isoformat()}"""] + f"""{datetime.now().isoformat()}""" ) await asyncio.sleep(1) yield DatastarStreamingResponse.merge_signals({"currentTime": f"{datetime.now().isoformat()}"}) diff --git a/examples/python/fasthtml/advanced.py b/examples/python/fasthtml/advanced.py index 42ad3b4f..c406f2fa 100644 --- a/examples/python/fasthtml/advanced.py +++ b/examples/python/fasthtml/advanced.py @@ -93,7 +93,7 @@ def GreatTable(pattern=default_pattern): @app.post async def table(filter: str): async def _(): - yield DatastarStreamingResponse.merge_fragments([GreatTable(filter)]) + yield DatastarStreamingResponse.merge_fragments(GreatTable(filter)) return DatastarStreamingResponse(_()) @@ -151,7 +151,7 @@ def index(): async def clock(): while True: now = datetime.isoformat(datetime.now()) - yield DatastarStreamingResponse.merge_fragments([Span(id="currentTime")(now)]) + yield DatastarStreamingResponse.merge_fragments(Span(id="currentTime")(now)) await asyncio.sleep(1) @@ -165,7 +165,7 @@ async def hello(): async def _(): # Simulate load time await asyncio.sleep(1) - yield DatastarStreamingResponse.merge_fragments([HELLO_BUTTON]) + yield DatastarStreamingResponse.merge_fragments(HELLO_BUTTON) return DatastarStreamingResponse(_()) @@ -185,7 +185,7 @@ async def reset(): async def _(sse): await asyncio.sleep(1) - yield sse.merge_fragments([reset_and_hello]) + yield sse.merge_fragments(reset_and_hello) return DatastarFastHTMLResponse(_) diff --git a/examples/python/fasthtml/simple.py b/examples/python/fasthtml/simple.py index 406dd4eb..75beebca 100644 --- a/examples/python/fasthtml/simple.py +++ b/examples/python/fasthtml/simple.py @@ -46,7 +46,7 @@ async def index(): async def clock(): while True: now = datetime.isoformat(datetime.now()) - yield DatastarStreamingResponse.merge_fragments([Span(id="currentTime")(now)]) + yield DatastarStreamingResponse.merge_fragments(Span(id="currentTime")(now)) await asyncio.sleep(1) yield DatastarStreamingResponse.merge_signals({"currentTime": f"{now}"}) await asyncio.sleep(1) diff --git a/examples/python/quart/app.py b/examples/python/quart/app.py index d2d088ea..87e3e3a0 100644 --- a/examples/python/quart/app.py +++ b/examples/python/quart/app.py @@ -47,7 +47,7 @@ async def updates(): async def time_updates(): while True: yield ServerSentEventGenerator.merge_fragments( - [f"""{datetime.now().isoformat()}"""] + f"""{datetime.now().isoformat()}""" ) await asyncio.sleep(1) yield ServerSentEventGenerator.merge_signals({"currentTime": f"{datetime.now().isoformat()}"}) diff --git a/examples/python/sanic/app.py b/examples/python/sanic/app.py index 465f9dd1..74897ef9 100644 --- a/examples/python/sanic/app.py +++ b/examples/python/sanic/app.py @@ -58,13 +58,11 @@ async def add_signal(request): await response.send( SSE.merge_fragments( - [ - """ + """
Current time from signal: CURRENT_TIME
- """ - ], + """, selector="#timers", merge_mode=FragmentMergeMode.FragmentMergeModeAppend, ) @@ -79,13 +77,11 @@ async def add_fragment(request): await response.send( SSE.merge_fragments( - [ - f"""\ + f"""\
Current time from fragment: {datetime.now().isoformat()}
- """ - ], + """, selector="#timers", merge_mode=FragmentMergeMode.FragmentMergeModeAppend, ) @@ -101,13 +97,11 @@ async def updates(request): while True: await response.send( SSE.merge_fragments( - [ - f""" + f"""
Current time from fragment: {datetime.now().isoformat()}
- """ - ], + """, selector=".fragment", ) ) diff --git a/sdk/python/src/datastar_py/fasthtml.py b/sdk/python/src/datastar_py/fasthtml.py index 8d2848c3..2ba32078 100644 --- a/sdk/python/src/datastar_py/fasthtml.py +++ b/sdk/python/src/datastar_py/fasthtml.py @@ -10,6 +10,7 @@ class DatastarStreamingResponse(_DatastarStreamingResponse): @classmethod @override def merge_fragments(cls, fragments, *args, **kwargs): - xml_fragments = [f if isinstance(f, str) else to_xml(f) for f in fragments] + if not isinstance(fragments, str): + fragments = to_xml(fragments) # From here, business as usual - return super().merge_fragments(xml_fragments, *args, **kwargs) + return super().merge_fragments(fragments, *args, **kwargs) diff --git a/sdk/python/src/datastar_py/sse.py b/sdk/python/src/datastar_py/sse.py index 858c9fd9..0e81772e 100644 --- a/sdk/python/src/datastar_py/sse.py +++ b/sdk/python/src/datastar_py/sse.py @@ -38,7 +38,7 @@ class ServerSentEventGenerator: @classmethod def merge_fragments( cls, - fragments: list[str], + fragments: str, selector: Optional[str] = None, merge_mode: Optional[consts.FragmentMergeMode] = None, use_view_transition: bool = consts.DEFAULT_FRAGMENTS_USE_VIEW_TRANSITIONS, @@ -57,8 +57,7 @@ class ServerSentEventGenerator: data_lines.extend( f"data: {consts.FRAGMENTS_DATALINE_LITERAL} {x}" - for fragment in fragments - for x in fragment.splitlines() + for x in fragments.splitlines() ) return ServerSentEventGenerator._send( From 7b706e126b607cb43b447ef6b40b837edde821a9 Mon Sep 17 00:00:00 2001 From: Dmitry Kotik <7944694+dkotik@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:07:41 -0400 Subject: [PATCH 2/2] externalize interface compatibility for two optional template engines (#826) Templ and GoStar are currently module dependencies for Golang SDK. They should not be required for 1.0 release. This step replaces two component interfaces with copies that ensure compatibility without having to include various optional engines as dependencies with each Datastar deployment. --- sdk/go/fragments-sugar.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/sdk/go/fragments-sugar.go b/sdk/go/fragments-sugar.go index 182bbd4a..27caff7e 100644 --- a/sdk/go/fragments-sugar.go +++ b/sdk/go/fragments-sugar.go @@ -1,10 +1,10 @@ package datastar import ( + "context" "fmt" + "io" - "github.com/a-h/templ" - "github.com/delaneyj/gostar/elements" "github.com/valyala/bytebufferpool" ) @@ -80,7 +80,17 @@ func (sse *ServerSentEventGenerator) MergeFragmentf(format string, args ...any) return sse.MergeFragments(fmt.Sprintf(format, args...)) } -func (sse *ServerSentEventGenerator) MergeFragmentTempl(c templ.Component, opts ...MergeFragmentOption) error { +// TemplComponent satisfies the component rendering interface for HTML template engine [Templ]. +// This separate type ensures compatibility with [Templ] without imposing a dependency requirement +// on those who prefer to use a different template engine. +// +// [Templ]: https://templ.guide/ +type TemplComponent interface { + Render(ctx context.Context, w io.Writer) error +} + +// MergeFragmentTempl is a convenience adaptor of [sse.MergeFragments] for [TemplComponent]. +func (sse *ServerSentEventGenerator) MergeFragmentTempl(c TemplComponent, opts ...MergeFragmentOption) error { buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) if err := c.Render(sse.Context(), buf); err != nil { @@ -92,7 +102,17 @@ func (sse *ServerSentEventGenerator) MergeFragmentTempl(c templ.Component, opts return nil } -func (sse *ServerSentEventGenerator) MergeFragmentGostar(child elements.ElementRenderer, opts ...MergeFragmentOption) error { +// GoStarElementRenderer satisfies the component rendering interface for HTML template engine [GoStar]. +// This separate type ensures compatibility with [GoStar] without imposing a dependency requirement +// on those who prefer to use a different template engine. +// +// [GoStar]: https://github.com/delaneyj/gostar +type GoStarElementRenderer interface { + Render(w io.Writer) error +} + +// MergeFragmentGostar is a convenience adaptor of [sse.MergeFragments] for [GoStarElementRenderer]. +func (sse *ServerSentEventGenerator) MergeFragmentGostar(child GoStarElementRenderer, opts ...MergeFragmentOption) error { buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) if err := child.Render(buf); err != nil {