diff --git a/README.md b/README.md index 9d9ac3c..d4d572e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ available in this package. import asyncio from datetime import datetime +import datastar_py from datastar_py import ServerSentEventGenerator as SSE, attribute_generator as data from datastar_py.quart import datastar_response, read_signals from quart import Quart @@ -43,7 +44,7 @@ def index(): return f""" - +
diff --git a/src/datastar_py/__init__.py b/src/datastar_py/__init__.py index c041c24..f1c4f58 100644 --- a/src/datastar_py/__init__.py +++ b/src/datastar_py/__init__.py @@ -1,13 +1,41 @@ from __future__ import annotations import json +import re from collections.abc import Mapping from typing import Any from .attributes import action_generator, attribute_generator from .sse import SSE_HEADERS, ServerSentEventGenerator -__all__ = ["SSE_HEADERS", "ServerSentEventGenerator", "action_generator", "attribute_generator"] +__all__ = [ + "SSE_HEADERS", + "ServerSentEventGenerator", + "action_generator", + "attribute_generator", + "url", +] + + +def url(version: str = "") -> str: + """Return the jsDelivr URL for the Datastar JavaScript module. + + Accepts major, minor, or exact versions with an optional ``v`` prefix. + Major, minor, and omitted versions can resolve to the latest release. + """ + if not isinstance(version, str): + raise TypeError("version must be a string or None") + ref = "" + if version != "": + if not re.fullmatch( + r"v?[0-9]+(?:\.[0-9]+(?:\.[0-9]+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?)?)?", + version.strip(), + ): + raise ValueError( + "version must be a valid jsdelivr compatible string, such as 'v1', '1.0', or '1.0.4'" + ) + ref = f"@v{version.strip().removeprefix('v')}" + return f"https://cdn.jsdelivr.net/gh/starfederation/datastar{ref}/bundles/datastar.js" def _read_signals( diff --git a/tests/test_url.py b/tests/test_url.py new file mode 100644 index 0000000..9d9686f --- /dev/null +++ b/tests/test_url.py @@ -0,0 +1,57 @@ +import pytest + +import datastar_py + + +@pytest.mark.parametrize( + ("version", "ref"), + ( + ("", ""), + ("1", "@v1"), + ("v1", "@v1"), + ("1.0", "@v1.0"), + ("v1.0", "@v1.0"), + ("1.0.4", "@v1.0.4"), + ("v1.0.4", "@v1.0.4"), + ("1.0.0-RC.7", "@v1.0.0-RC.7"), + ("v1.0.0-RC.7", "@v1.0.0-RC.7"), + ), +) +def test_url_versions(version, ref): + assert datastar_py.url(version) == ( + f"https://cdn.jsdelivr.net/gh/starfederation/datastar{ref}/bundles/datastar.js" + ) + + +def test_url_default(): + assert datastar_py.url() == ( + "https://cdn.jsdelivr.net/gh/starfederation/datastar/bundles/datastar.js" + ) + + +@pytest.mark.parametrize( + "version", + ( + "v", + "v1.", + "1..0", + "1.0.4.1", + "1-RC.7", + "1.0-RC.7", + "1.0.4-", + "1.0.4-RC.", + "latest", + "v1/something", + "v1?x=1", + "v1#fragment", + ), +) +def test_url_rejects_invalid_versions(version): + with pytest.raises(ValueError, match="version"): + datastar_py.url(version) + + +@pytest.mark.parametrize("version", (1, 1.0, True, b"v1", [], {})) +def test_url_rejects_invalid_types(version): + with pytest.raises(TypeError, match="version must be a string or None"): + datastar_py.url(version)