├── py.typed ├── tests ├── __init__.py ├── cli │ ├── __init__.py │ └── test_cli.py ├── html │ ├── __init__.py │ ├── django │ │ ├── __init__.py │ │ └── test_variable.py │ └── unicorn │ │ ├── __init__.py │ │ └── test_action.py ├── main │ ├── __init__.py │ ├── test_runserver.py │ ├── test_configure.py │ └── test_init.py ├── views │ ├── __init__.py │ ├── test_get_default_index_template_html.py │ └── test_index.py ├── components │ ├── __init__.py │ └── test_get_component_classes.py ├── settings │ ├── __init__.py │ ├── test_get_components_setting.py │ ├── test_get_settings.py │ └── test_get_templates_setting.py ├── templates │ ├── unicorn │ │ ├── bad-fake-view.html │ │ └── fake-view.html │ └── base.html ├── urls.py └── fake_components.py ├── CHANGELOG.md ├── .github └── FUNDING.yml ├── src └── django_unicorn_playground │ ├── html │ ├── __init__.py │ ├── django.py │ └── unicorn.py │ ├── __init__.py │ ├── templates │ └── base.html │ ├── urls.py │ ├── cli.py │ ├── components.py │ ├── views.py │ ├── main.py │ └── settings.py ├── .gitignore ├── playground-demo.mp4 ├── examples └── counter.py ├── LICENSE ├── conftest.py ├── justfile ├── pyproject.toml ├── README.md └── poetry.lock /py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/html/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/main/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | -------------------------------------------------------------------------------- /tests/components/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/html/django/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/html/unicorn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [adamghill] 2 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/html/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /tests/templates/unicorn/bad-fake-view.html: -------------------------------------------------------------------------------- 1 | bad-fake-view -------------------------------------------------------------------------------- /tests/templates/unicorn/fake-view.html: -------------------------------------------------------------------------------- 1 |
2 | fake-view 3 |
-------------------------------------------------------------------------------- /playground-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn-playground/main/playground-demo.mp4 -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | urlpatterns = (path("", include("django_unicorn.urls")),) 4 | -------------------------------------------------------------------------------- /tests/fake_components.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | 3 | 4 | class FakeView(UnicornView): 5 | pass 6 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/__init__.py: -------------------------------------------------------------------------------- 1 | from django_unicorn_playground.main import UnicornPlayground 2 | 3 | __all__ = [ 4 | "UnicornPlayground", 5 | ] 6 | -------------------------------------------------------------------------------- /tests/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% unicorn_scripts %} 5 | 6 | 7 | {% block content %} 8 | {% endblock content %} 9 | 10 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% unicorn_scripts %} 5 | 6 | 7 | {% block content %} 8 | {% endblock content %} 9 | 10 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from django_unicorn_playground.views import index 4 | 5 | urlpatterns = [ 6 | path("", index), 7 | path("unicorn/", include("django_unicorn.urls")), 8 | ] 9 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/html/django.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | def variable(var: Any) -> str: 5 | """Output a var as a DTL variable.""" 6 | 7 | if var is None: 8 | return "" 9 | 10 | # TODO: Add filters? 11 | 12 | return "{{ " + str(var) + " }}" 13 | -------------------------------------------------------------------------------- /tests/settings/test_get_components_setting.py: -------------------------------------------------------------------------------- 1 | from django_unicorn_playground.settings import _get_components_setting 2 | 3 | from ..fake_components import FakeView 4 | 5 | 6 | def test_get_components_setting(): 7 | expected = {"tests.fake_components": FakeView} 8 | actual = _get_components_setting([FakeView]) 9 | 10 | assert expected == actual 11 | -------------------------------------------------------------------------------- /tests/main/test_runserver.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django_unicorn_playground.main import UnicornPlayground 4 | 5 | 6 | @patch("django_unicorn_playground.main.execute_from_command_line") 7 | @patch("django_unicorn_playground.main.django_conf_settings") 8 | def test_runserver(django_conf_settings, execute_from_command_line): 9 | UnicornPlayground("tests/fake_components.py").runserver() 10 | 11 | execute_from_command_line.assert_called_once_with(["manage", "runserver", "8000"]) 12 | -------------------------------------------------------------------------------- /tests/html/django/test_variable.py: -------------------------------------------------------------------------------- 1 | from django_unicorn_playground.html.django import variable 2 | 3 | 4 | def test_variable_str(): 5 | expected = "{{ asdf }}" 6 | actual = variable("asdf") 7 | 8 | assert expected == actual 9 | 10 | 11 | def test_variable_int(): 12 | expected = "{{ 1 }}" 13 | actual = variable(1) 14 | 15 | assert expected == actual 16 | 17 | 18 | def test_variable_none(): 19 | expected = "" 20 | actual = variable(None) 21 | 22 | assert expected == actual 23 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/cli.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import rich_click as click 4 | 5 | from django_unicorn_playground import UnicornPlayground 6 | 7 | 8 | @click.command() 9 | @click.argument("component", type=click.Path(exists=True, dir_okay=False, path_type=Path)) 10 | @click.option("--port", type=int, default=8000, help="Port for the developer webserver.") 11 | @click.option( 12 | "--template_dir", type=click.Path(exists=True, dir_okay=True, path_type=Path), help="Directory for templates." 13 | ) 14 | @click.version_option() 15 | def cli(component: Path, template_dir: Path, port: int): 16 | UnicornPlayground(component_path=component, template_dir=template_dir).runserver(port=port) 17 | -------------------------------------------------------------------------------- /tests/cli/test_cli.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from django_unicorn_playground.cli import cli 4 | 5 | 6 | def test_invalid_component_path(): 7 | runner = CliRunner(mix_stderr=False) 8 | 9 | result = runner.invoke( 10 | cli, 11 | [ 12 | "tests/invalid_component_path.py", 13 | ], 14 | ) 15 | 16 | assert result.exit_code == 2 17 | assert "Invalid value for 'COMPONENT'" in result.stderr 18 | 19 | 20 | def test_no_component_classes(): 21 | runner = CliRunner() 22 | 23 | result = runner.invoke( 24 | cli, 25 | [ 26 | "tests/__init__.py", 27 | ], 28 | ) 29 | 30 | assert result.exit_code == 1 31 | assert "No subclass of UnicornView found" in str(result.exception) 32 | -------------------------------------------------------------------------------- /examples/counter.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "django_unicorn_playground" 5 | # ] 6 | # /// 7 | 8 | from django_unicorn.components import UnicornView 9 | 10 | 11 | class CounterView(UnicornView): 12 | template_html = """
13 | 14 | 15 | 16 |
17 | {{ count }} 18 |
19 |
20 | """ 21 | 22 | count: int = 0 23 | 24 | def increment(self): 25 | self.count += 1 26 | 27 | def decrement(self): 28 | self.count -= 1 29 | 30 | 31 | if __name__ == "__main__": 32 | from django_unicorn_playground import UnicornPlayground 33 | 34 | UnicornPlayground(__file__).runserver() 35 | -------------------------------------------------------------------------------- /tests/views/test_get_default_index_template_html.py: -------------------------------------------------------------------------------- 1 | from django_unicorn_playground.views import _get_default_index_template_html 2 | from tests.fake_components import FakeView 3 | 4 | 5 | def test_get_default_index_template_html(): 6 | expected = """{% extends "base.html" %} 7 | {% block content %} 8 | 9 | {% endblock content %}""" 10 | actual = _get_default_index_template_html(components={}) 11 | 12 | assert expected == actual 13 | 14 | 15 | def test_get_default_index_template_html_components(): 16 | expected = """{% extends "base.html" %} 17 | {% block content %} 18 | {% unicorn 'fake-component' %} 19 | 20 | {% endblock content %}""" 21 | 22 | components = {"fake-component": FakeView} 23 | actual = _get_default_index_template_html(components=components) 24 | 25 | assert expected == actual 26 | -------------------------------------------------------------------------------- /tests/main/test_configure.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY, patch 2 | 3 | from django_unicorn_playground import urls 4 | from django_unicorn_playground.main import UnicornPlayground 5 | 6 | 7 | @patch("django_unicorn_playground.main.django_conf_settings") 8 | def test_configure(django_conf_settings): 9 | UnicornPlayground("tests/fake_components.py") 10 | 11 | components = {"fake_components": ANY} 12 | 13 | django_conf_settings.configure.assert_called_once_with( 14 | ALLOWED_HOSTS="*", 15 | ROOT_URLCONF=urls, 16 | SECRET_KEY=ANY, 17 | DEBUG=True, 18 | TEMPLATES=[ 19 | { 20 | "BACKEND": "django.template.backends.django.DjangoTemplates", 21 | "DIRS": [ANY], 22 | "APP_DIRS": True, 23 | "OPTIONS": {"builtins": ["django_unicorn.templatetags.unicorn"]}, 24 | } 25 | ], 26 | INSTALLED_APPS=("django.contrib.staticfiles", "django_unicorn", "django_unicorn_playground"), 27 | UNICORN={"COMPONENTS": components}, 28 | STATIC_URL="static/", 29 | ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adam Hill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from django.conf import settings 4 | 5 | 6 | def pytest_configure(): 7 | base_dir = Path(".") 8 | 9 | settings.configure( 10 | BASE_DIR=base_dir, 11 | SECRET_KEY="this-is-a-secret", 12 | TEMPLATES=[ 13 | { 14 | "BACKEND": "django.template.backends.django.DjangoTemplates", 15 | "APP_DIRS": True, 16 | "OPTIONS": { 17 | "context_processors": [ 18 | "django.template.context_processors.request", 19 | "django.template.context_processors.debug", 20 | "django.template.context_processors.static", 21 | ], 22 | "builtins": [ 23 | "django_unicorn.templatetags.unicorn", 24 | ], 25 | }, 26 | } 27 | ], 28 | INSTALLED_APPS=[ 29 | "django_unicorn", 30 | "tests", 31 | ], 32 | CACHES={ 33 | "default": { 34 | "BACKEND": "django.core.cache.backends.dummy.DummyCache", 35 | } 36 | }, 37 | UNICORN={}, 38 | ROOT_URLCONF="tests.urls", 39 | ) 40 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/components.py: -------------------------------------------------------------------------------- 1 | import importlib.machinery 2 | import inspect 3 | from pathlib import Path 4 | 5 | from django_unicorn.components import UnicornView 6 | from typeguard import typechecked 7 | 8 | 9 | @typechecked 10 | def get_component_classes(component_path: Path | str) -> list[type[UnicornView]]: 11 | """Create the component class based on the path of the component.""" 12 | 13 | if not component_path: 14 | raise AssertionError("A component path must be passed in") 15 | 16 | if isinstance(component_path, str): 17 | component_path = Path(component_path) 18 | 19 | module = component_path.name.replace(component_path.suffix, "") 20 | 21 | # Inspect the passed-in component file and get all of the imported classes 22 | module = importlib.machinery.SourceFileLoader(module, str(component_path)).load_module() 23 | class_members = inspect.getmembers(module, inspect.isclass) 24 | 25 | # Get the UnicornView subclass in the component file 26 | unicorn_view_subclasses = [c[1] for c in class_members if issubclass(c[1], UnicornView) and c[1] is not UnicornView] 27 | 28 | if not unicorn_view_subclasses: 29 | raise AssertionError("No subclass of UnicornView found") 30 | 31 | return unicorn_view_subclasses 32 | -------------------------------------------------------------------------------- /tests/settings/test_get_settings.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | from django_unicorn_playground.settings import get_settings 4 | 5 | from ..fake_components import FakeView 6 | 7 | 8 | def test_get_settings(): 9 | expected = { 10 | "ALLOWED_HOSTS": "*", 11 | "ROOT_URLCONF": ANY, 12 | "SECRET_KEY": ANY, 13 | "DEBUG": True, 14 | "TEMPLATES": ANY, 15 | "INSTALLED_APPS": ("django.contrib.staticfiles", "django_unicorn", "django_unicorn_playground"), 16 | "UNICORN": {"COMPONENTS": {}}, 17 | "STATIC_URL": "static/", 18 | } 19 | actual = get_settings(template_dir=None, component_classes=[]) 20 | 21 | assert expected == actual 22 | 23 | 24 | def test_get_settings_component_classes(): 25 | expected = { 26 | "ALLOWED_HOSTS": "*", 27 | "ROOT_URLCONF": ANY, 28 | "SECRET_KEY": ANY, 29 | "DEBUG": True, 30 | "TEMPLATES": ANY, 31 | "INSTALLED_APPS": ("django.contrib.staticfiles", "django_unicorn", "django_unicorn_playground"), 32 | "UNICORN": {"COMPONENTS": {"tests.fake_components": FakeView}}, 33 | "STATIC_URL": "static/", 34 | } 35 | 36 | actual = get_settings(template_dir=None, component_classes=[FakeView]) 37 | 38 | assert expected == actual 39 | -------------------------------------------------------------------------------- /tests/components/test_get_component_classes.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | import typeguard 5 | from django_unicorn.components import UnicornView 6 | 7 | from django_unicorn_playground.components import get_component_classes 8 | 9 | 10 | def test_get_component_classes_str(): 11 | actual = get_component_classes("tests/fake_components.py") 12 | 13 | assert len(actual) == 1 14 | 15 | component_class = actual[0] 16 | assert issubclass(component_class, UnicornView) 17 | 18 | assert component_class.__name__ == "FakeView" 19 | 20 | 21 | def test_get_component_classes_path(): 22 | actual = get_component_classes(Path("tests/fake_components.py")) 23 | 24 | assert len(actual) == 1 25 | 26 | component_class = actual[0] 27 | assert issubclass(component_class, UnicornView) 28 | 29 | assert component_class.__name__ == "FakeView" 30 | 31 | 32 | def test_get_component_classes_none(): 33 | with pytest.raises(typeguard.TypeCheckError): 34 | get_component_classes(None) 35 | 36 | 37 | def test_get_component_classes_empty_str(): 38 | with pytest.raises(AssertionError) as e: 39 | get_component_classes("") 40 | 41 | assert e.exconly() == "AssertionError: A component path must be passed in" 42 | 43 | 44 | def test_get_component_classes_no_unicorn_views(): 45 | with pytest.raises(AssertionError) as e: 46 | get_component_classes(__file__) 47 | 48 | assert e.exconly() == "AssertionError: No subclass of UnicornView found" 49 | -------------------------------------------------------------------------------- /tests/html/unicorn/test_action.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import typeguard 3 | 4 | from django_unicorn_playground.html.unicorn import action, blur, change, click, focus, input, submit 5 | 6 | 7 | def fake_method(): 8 | pass 9 | 10 | 11 | def test_action(): 12 | expected = {"unicorn:click": "test"} 13 | actual = action("click", "test") 14 | 15 | assert expected == actual 16 | 17 | 18 | def test_action_method(): 19 | expected = {"unicorn:click": "fake_method"} 20 | actual = action("click", fake_method) 21 | 22 | assert expected == actual 23 | 24 | 25 | def test_input(): 26 | expected = {"unicorn:input": "test"} 27 | actual = input("test") 28 | 29 | assert expected == actual 30 | 31 | 32 | def test_focus(): 33 | expected = {"unicorn:focus": "test"} 34 | actual = focus("test") 35 | 36 | assert expected == actual 37 | 38 | 39 | def test_change(): 40 | expected = {"unicorn:change": "test"} 41 | actual = change("test") 42 | 43 | assert expected == actual 44 | 45 | 46 | def test_blur(): 47 | expected = {"unicorn:blur": "test"} 48 | actual = blur("test") 49 | 50 | assert expected == actual 51 | 52 | 53 | def test_submit(): 54 | expected = {"unicorn:submit": "test"} 55 | actual = submit("test") 56 | 57 | assert expected == actual 58 | 59 | 60 | def test_click(): 61 | expected = {"unicorn:click": "test"} 62 | actual = click("test") 63 | 64 | assert expected == actual 65 | 66 | 67 | def test_action_invalid_event(): 68 | with pytest.raises(typeguard.TypeCheckError): 69 | action("asdf", "test") 70 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse 3 | from django.shortcuts import render 4 | from django.template.exceptions import TemplateDoesNotExist 5 | from django_unicorn.components import UnicornView 6 | from django_unicorn.utils import create_template 7 | from typeguard import typechecked 8 | 9 | 10 | @typechecked 11 | def _get_default_index_template_html(components: dict[str, type[UnicornView]]) -> str: 12 | """Gets the default index template HTML with all components.""" 13 | 14 | html = """{% extends "base.html" %} 15 | {% block content %} 16 | """ 17 | 18 | for component_name in components.keys(): 19 | html += "{% unicorn '" + component_name + "' %}\n" 20 | 21 | html += """ 22 | {% endblock content %}""" 23 | 24 | return html 25 | 26 | 27 | def index(request): 28 | """Default route for the developer server.""" 29 | 30 | try: 31 | return render(request, "index.html") 32 | except TemplateDoesNotExist as e: 33 | if not hasattr(settings, "UNICORN"): 34 | raise AssertionError("Missing UNICORN setting") from e 35 | 36 | components = settings.UNICORN.get("COMPONENTS", {}) 37 | 38 | if not components: 39 | raise AssertionError("No components could be found") from e 40 | 41 | index_template_html = _get_default_index_template_html(components) 42 | template = create_template(index_template_html, engine_name="django") 43 | content = template.render(context={"components": components}) 44 | 45 | return HttpResponse(content) 46 | -------------------------------------------------------------------------------- /tests/views/test_index.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django_unicorn.errors import MissingComponentElementError 3 | 4 | from django_unicorn_playground.views import index 5 | from tests.fake_components import FakeView 6 | 7 | 8 | def test_index(rf, settings): 9 | settings.UNICORN = {"COMPONENTS": {"fake-view": FakeView}} 10 | 11 | request = rf.get("/") 12 | 13 | response = index(request) 14 | assert response.status_code == 200 15 | 16 | settings.UNICORN = {} 17 | 18 | 19 | def test_index_invalid_template(rf, settings): 20 | settings.UNICORN = {"COMPONENTS": {"bad-fake-view": FakeView}} 21 | 22 | request = rf.get("/") 23 | 24 | with pytest.raises(MissingComponentElementError) as e: 25 | index(request) 26 | 27 | assert ( 28 | e.exconly() == "django_unicorn.errors.MissingComponentElementError: No root element for the component was found" 29 | ) 30 | 31 | settings.UNICORN = {} 32 | 33 | 34 | def test_index_missing_unicorn_setting(rf, settings): 35 | del settings.UNICORN 36 | 37 | request = rf.get("/") 38 | 39 | with pytest.raises(AssertionError) as e: 40 | index(request) 41 | 42 | assert e.exconly() == "AssertionError: Missing UNICORN setting" 43 | 44 | settings.UNICORN = {} 45 | 46 | 47 | def test_index_missing_unicorn_components(rf, settings): 48 | settings.UNICORN = {"COMPONENTS": {}} 49 | 50 | request = rf.get("/") 51 | 52 | with pytest.raises(AssertionError) as e: 53 | index(request) 54 | 55 | assert e.exconly() == "AssertionError: No components could be found" 56 | 57 | settings.UNICORN = {} 58 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set quiet 2 | set dotenv-load 3 | set export 4 | 5 | # List commands 6 | _default: 7 | just --list --unsorted --justfile {{justfile()}} --list-heading $'Available commands:\n' 8 | 9 | # Install dependencies 10 | bootstrap: 11 | poetry install 12 | 13 | # Set up the project 14 | setup: 15 | brew install pipx 16 | pipx ensurepath 17 | pipx install poetry 18 | pipx install ruff 19 | 20 | # Update the project 21 | update: 22 | just lock 23 | poetry install 24 | 25 | # Lock the dependencies 26 | lock: 27 | poetry lock 28 | 29 | # Lint the project 30 | lint *ARGS='.': 31 | -ruff check {{ ARGS }} 32 | 33 | # Check the types in the project 34 | type *ARGS='': 35 | -poetry run mypy {{ ARGS }} # need to run through poetry to see installed dependencies 36 | 37 | # Benchmark the project 38 | # benchmark: 39 | # -poetry run pytest tests/benchmarks/ --benchmark-only --benchmark-compare 40 | 41 | # Run the tests 42 | test *ARGS='': 43 | -poetry run pytest {{ ARGS }} 44 | 45 | alias t := test 46 | 47 | # Run coverage on the code 48 | coverage: 49 | -poetry run pytest --cov-report term-missing --cov=src/django_unicorn_playground 50 | 51 | # Run all the dev things 52 | dev: 53 | just lint 54 | just type 55 | just coverage 56 | 57 | # Serve the component via pipx 58 | serve *ARGS: 59 | just build 60 | pipx run --no-cache {{ ARGS }} 61 | 62 | # Serve the component via the `unicorn` package 63 | run *ARGS: 64 | poetry run unicorn {{ ARGS }} 65 | 66 | # Build the package 67 | build: 68 | poetry build 69 | 70 | # Build and publish the package to test PyPI and prod PyPI 71 | publish: 72 | poetry publish --build -r test 73 | poetry publish 74 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from django.conf import settings as django_conf_settings 5 | from django.core.management import execute_from_command_line 6 | from typeguard import typechecked 7 | 8 | from django_unicorn_playground.components import get_component_classes 9 | from django_unicorn_playground.settings import get_settings 10 | 11 | BASE_PATH = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 12 | 13 | 14 | @typechecked 15 | class UnicornPlayground: 16 | def __init__( 17 | self, 18 | component_path: Path | str, 19 | *, 20 | template_dir: Path | str | None = None, 21 | **django_settings, 22 | ): 23 | """ 24 | Initialize a new django-unicorn playground. 25 | 26 | Args: 27 | component_path: Where the component lives on the filesystem. 28 | template_dir: Used to specify a template directory. 29 | django_settings: Override the default Django settings. 30 | """ 31 | 32 | component_classes = get_component_classes(component_path) 33 | 34 | # Get default Django settings 35 | self.settings = get_settings(template_dir, component_classes) 36 | 37 | # Override default settings with init kwargs 38 | self.settings.update(**django_settings) 39 | 40 | self.configure() 41 | 42 | def configure(self): 43 | """Configures Django with the settings.""" 44 | 45 | django_conf_settings.configure(**self.settings) 46 | 47 | def runserver(self, *, port: int = 8000): 48 | """Starts the dev server. 49 | 50 | Args: 51 | port: The port that the dev server should be run on. 52 | """ 53 | 54 | execute_from_command_line(["manage", "runserver", str(port)]) 55 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/settings.py: -------------------------------------------------------------------------------- 1 | from os import getcwd 2 | from pathlib import Path 3 | from typing import Any 4 | from uuid import uuid4 5 | 6 | from django_unicorn.components import UnicornView 7 | from typeguard import typechecked 8 | 9 | from django_unicorn_playground import urls 10 | 11 | 12 | @typechecked 13 | def _get_components_setting(component_classes: list[type[UnicornView]]) -> dict[str, type[UnicornView]]: 14 | """Gets the `UNICORN.COMPONENTS` Django settings.""" 15 | 16 | components = {} 17 | 18 | for component_class in component_classes: 19 | component_name = component_class.__module__ 20 | components[component_name] = component_class 21 | 22 | return components 23 | 24 | 25 | @typechecked 26 | def _get_templates_setting(template_dir: Path | str | None = None) -> list[dict[str, Any]]: 27 | """Gets the `TEMPLATES` Django settings.""" 28 | 29 | template_dirs = [getcwd()] 30 | 31 | if template_dir: 32 | template_dirs.append(str(template_dir)) 33 | 34 | templates = [ 35 | { 36 | "BACKEND": "django.template.backends.django.DjangoTemplates", 37 | "DIRS": template_dirs, 38 | "APP_DIRS": True, 39 | "OPTIONS": { 40 | "builtins": [ 41 | "django_unicorn.templatetags.unicorn", 42 | ], 43 | }, 44 | }, 45 | ] 46 | 47 | return templates 48 | 49 | 50 | @typechecked 51 | def get_settings(template_dir: Path | str | None, component_classes: list[type[UnicornView]]) -> dict[str, Any]: 52 | return { 53 | "ALLOWED_HOSTS": "*", 54 | "ROOT_URLCONF": urls, 55 | "SECRET_KEY": str(uuid4()), 56 | "DEBUG": True, 57 | "TEMPLATES": _get_templates_setting(template_dir), 58 | "INSTALLED_APPS": ( 59 | "django.contrib.staticfiles", # required for django-unicorn JavaScript 60 | "django_unicorn", 61 | "django_unicorn_playground", 62 | ), 63 | "UNICORN": { 64 | "COMPONENTS": _get_components_setting(component_classes), 65 | }, 66 | "STATIC_URL": "static/", # required for django-unicorn JavaScript 67 | } 68 | -------------------------------------------------------------------------------- /src/django_unicorn_playground/html/unicorn.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Literal 3 | 4 | from typeguard import typechecked 5 | 6 | EVENT_LITERALS = Literal[ 7 | "DOMContentLoaded", 8 | "afterprint", 9 | "beforeprint", 10 | "beforematch", 11 | "beforetoggle", 12 | "beforeunload", 13 | "blur", 14 | "cancel", 15 | "change", 16 | "click", 17 | "close", 18 | "connect", 19 | "contextlost", 20 | "contextrestored", 21 | "currententrychange", 22 | "dispose", 23 | "error", 24 | "focus", 25 | "formdata", 26 | "hashchange", 27 | "input", 28 | "invalid", 29 | "languagechange", 30 | "load", 31 | "message", 32 | "messageerror", 33 | "navigate", 34 | "navigateerror", 35 | "navigatesuccess", 36 | "offline", 37 | "online", 38 | "open", 39 | "pageswap", 40 | "pagehide", 41 | "pagereveal", 42 | "pageshow", 43 | "pointercancel", 44 | "popstate", 45 | "readystatechange", 46 | "rejectionhandled", 47 | "reset", 48 | "select", 49 | "storage", 50 | "submit", 51 | "toggle", 52 | "unhandledrejection", 53 | "unload", 54 | "visibilitychange", 55 | ] 56 | 57 | 58 | @typechecked 59 | def click(method: Callable | str) -> dict: 60 | """Output the attribute for a click action.""" 61 | 62 | return action("click", method) 63 | 64 | 65 | @typechecked 66 | def submit(method: Callable | str) -> dict: 67 | """Output the attribute for a submit action.""" 68 | 69 | return action("submit", method) 70 | 71 | 72 | @typechecked 73 | def blur(method: Callable | str) -> dict: 74 | """Output the attribute for a blur action.""" 75 | 76 | return action("blur", method) 77 | 78 | 79 | @typechecked 80 | def change(method: Callable | str) -> dict: 81 | """Output the attribute for a change action.""" 82 | 83 | return action("change", method) 84 | 85 | 86 | @typechecked 87 | def focus(method: Callable | str) -> dict: 88 | """Output the attribute for a focus action.""" 89 | 90 | return action("focus", method) 91 | 92 | 93 | @typechecked 94 | def input(method: Callable | str) -> dict: # noqa: A001 95 | """Output the attribute for a input action.""" 96 | 97 | return action("input", method) 98 | 99 | 100 | @typechecked 101 | def action(event: EVENT_LITERALS, method: Callable | str) -> dict: 102 | """Output the attribute for a generic event action.""" 103 | 104 | if callable(method): 105 | method = method.__name__ 106 | 107 | return {f"unicorn:{event}": method} 108 | -------------------------------------------------------------------------------- /tests/settings/test_get_templates_setting.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unittest.mock import ANY 3 | 4 | from django_unicorn_playground.settings import _get_templates_setting 5 | 6 | 7 | def test_get_templates_setting(): 8 | expected = [ 9 | { 10 | "APP_DIRS": True, 11 | "BACKEND": "django.template.backends.django.DjangoTemplates", 12 | "DIRS": [ANY], 13 | "OPTIONS": {"builtins": ["django_unicorn.templatetags.unicorn"]}, 14 | } 15 | ] 16 | actual = _get_templates_setting() 17 | 18 | assert expected == actual 19 | 20 | 21 | def test_get_templates_setting_cwd(monkeypatch): 22 | monkeypatch.setattr("django_unicorn_playground.settings.getcwd", lambda: "/test") 23 | 24 | expected = [ 25 | { 26 | "APP_DIRS": True, 27 | "BACKEND": "django.template.backends.django.DjangoTemplates", 28 | "DIRS": ["/test"], 29 | "OPTIONS": {"builtins": ["django_unicorn.templatetags.unicorn"]}, 30 | } 31 | ] 32 | actual = _get_templates_setting() 33 | 34 | assert expected == actual 35 | 36 | 37 | def test_get_templates_setting_template_dir_none(monkeypatch): 38 | monkeypatch.setattr("django_unicorn_playground.settings.getcwd", lambda: "/test") 39 | 40 | expected = [ 41 | { 42 | "APP_DIRS": True, 43 | "BACKEND": "django.template.backends.django.DjangoTemplates", 44 | "DIRS": ["/test"], 45 | "OPTIONS": {"builtins": ["django_unicorn.templatetags.unicorn"]}, 46 | } 47 | ] 48 | actual = _get_templates_setting(template_dir=None) 49 | 50 | assert expected == actual 51 | 52 | 53 | def test_get_templates_setting_template_dir_str(monkeypatch): 54 | monkeypatch.setattr("django_unicorn_playground.settings.getcwd", lambda: "/test") 55 | 56 | expected = [ 57 | { 58 | "APP_DIRS": True, 59 | "BACKEND": "django.template.backends.django.DjangoTemplates", 60 | "DIRS": ["/test", "/test"], 61 | "OPTIONS": {"builtins": ["django_unicorn.templatetags.unicorn"]}, 62 | } 63 | ] 64 | actual = _get_templates_setting(template_dir="/test") 65 | 66 | assert expected == actual 67 | 68 | 69 | def test_get_templates_setting_template_dir_path(monkeypatch): 70 | monkeypatch.setattr("django_unicorn_playground.settings.getcwd", lambda: "/test") 71 | 72 | expected = [ 73 | { 74 | "APP_DIRS": True, 75 | "BACKEND": "django.template.backends.django.DjangoTemplates", 76 | "DIRS": ["/test", "/test"], 77 | "OPTIONS": {"builtins": ["django_unicorn.templatetags.unicorn"]}, 78 | } 79 | ] 80 | actual = _get_templates_setting(template_dir=Path("/test")) 81 | 82 | assert expected == actual 83 | -------------------------------------------------------------------------------- /tests/main/test_init.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | import pytest 4 | 5 | from django_unicorn_playground.main import UnicornPlayground 6 | 7 | 8 | @pytest.fixture 9 | def playground_settings(): 10 | return { 11 | "ALLOWED_HOSTS": "*", 12 | "ROOT_URLCONF": ANY, 13 | "SECRET_KEY": ANY, 14 | "DEBUG": True, 15 | "TEMPLATES": [ 16 | { 17 | "BACKEND": "django.template.backends.django.DjangoTemplates", 18 | "DIRS": ANY, 19 | "APP_DIRS": True, 20 | "OPTIONS": {"builtins": ["django_unicorn.templatetags.unicorn"]}, 21 | } 22 | ], 23 | "INSTALLED_APPS": ("django.contrib.staticfiles", "django_unicorn", "django_unicorn_playground"), 24 | "UNICORN": {"COMPONENTS": {"fake_components": ANY}}, 25 | "STATIC_URL": "static/", 26 | }.copy() 27 | 28 | 29 | def test_init_no_component_path(): 30 | with pytest.raises(TypeError) as e: 31 | UnicornPlayground() 32 | 33 | assert ( 34 | e.exconly() 35 | == "TypeError: UnicornPlayground.__init__() missing 1 required positional argument: 'component_path'" 36 | ) 37 | 38 | 39 | def test_init(monkeypatch, playground_settings): 40 | monkeypatch.setattr("django_unicorn_playground.main.UnicornPlayground.configure", lambda s: None) 41 | 42 | actual = UnicornPlayground("tests/fake_components.py") 43 | 44 | assert playground_settings == actual.settings 45 | 46 | 47 | def test_init_template_dir(monkeypatch, playground_settings): 48 | monkeypatch.setattr("django_unicorn_playground.settings.getcwd", lambda: "/test") 49 | monkeypatch.setattr("django_unicorn_playground.main.UnicornPlayground.configure", lambda s: None) 50 | 51 | playground_settings["TEMPLATES"][0]["DIRS"] = [ 52 | "/test", 53 | "tests/templates", 54 | ] 55 | 56 | actual = UnicornPlayground("tests/fake_components.py", template_dir="tests/templates") 57 | 58 | assert playground_settings == actual.settings 59 | 60 | 61 | def test_init_override_setting(monkeypatch, playground_settings): 62 | monkeypatch.setattr("django_unicorn_playground.main.UnicornPlayground.configure", lambda s: None) 63 | 64 | playground_settings["STATIC_URL"] = "/test" 65 | 66 | actual = UnicornPlayground("tests/fake_components.py", STATIC_URL="/test") 67 | 68 | assert playground_settings == actual.settings 69 | 70 | 71 | def test_init_new_setting(monkeypatch, playground_settings): 72 | monkeypatch.setattr("django_unicorn_playground.main.UnicornPlayground.configure", lambda s: None) 73 | 74 | playground_settings["TEST_SETTING"] = "something-new" 75 | 76 | actual = UnicornPlayground("tests/fake_components.py", TEST_SETTING="something-new") 77 | 78 | assert playground_settings == actual.settings 79 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-unicorn-playground" 3 | version = "0.1.1" 4 | description = "Prototype and debug `Unicorn` components without creating a complete Django application." 5 | authors = ["adamghill "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/adamghill/django-unicorn-playground/" 9 | homepage = "https://github.com/adamghill/django-unicorn-playground/" 10 | documentation = "https://github.com/adamghill/django-unicorn-playground/" 11 | keywords = ["django", "python", "css", "html"] 12 | packages = [ 13 | { include = "django_unicorn_playground", from = "src" } 14 | ] 15 | 16 | [tool.poetry.urls] 17 | "Funding" = "https://github.com/sponsors/adamghill" 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.10" 21 | click = "^8.1.7" 22 | #django-unicorn = { path = "../django-unicorn/", develop = true } 23 | django-unicorn = ">=0.61.0" 24 | typeguard = "^4.3" 25 | rich-click = "^1.8" 26 | 27 | [tool.poetry.group.dev.dependencies] 28 | pytest = "^8" 29 | pytest-django = "^4" 30 | pytest-cov = "^5" 31 | django-stubs = "^5" 32 | 33 | [tool.poetry.scripts] 34 | unicorn = "django_unicorn_playground.cli:cli" 35 | 36 | [tool.ruff] 37 | src = ["django_unicorn_playground"] 38 | exclude = [] 39 | target-version = "py310" 40 | line-length = 120 41 | 42 | [tool.ruff.lint] 43 | select = [ 44 | "A", 45 | "ARG", 46 | "B", 47 | "C", 48 | "DTZ", 49 | "E", 50 | "EM", 51 | "F", 52 | "FBT", 53 | "I", 54 | "ICN", 55 | "ISC", 56 | "N", 57 | "PLC", 58 | "PLE", 59 | "PLR", 60 | "PLW", 61 | "Q", 62 | "RUF", 63 | "S", 64 | "T", 65 | "TID", 66 | "UP", 67 | "W", 68 | "YTT", 69 | ] 70 | ignore = [ 71 | # Allow non-abstract empty methods in abstract base classes 72 | "B027", 73 | # Allow boolean positional values in function calls, like `dict.get(... True)` 74 | "FBT003", 75 | # Ignore checks for possible passwords 76 | "S105", "S106", "S107", 77 | # Ignore complexity 78 | "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", 79 | # Ignore unused variables 80 | "F841", 81 | # Ignore exception strings 82 | "EM101", "EM102", 83 | ] 84 | unfixable = [ 85 | # Don't touch unused imports 86 | "F401", 87 | ] 88 | 89 | [tool.ruff.lint.isort] 90 | known-first-party = ["django_unicorn_playground"] 91 | 92 | [tool.ruff.lint.pydocstyle] 93 | convention = "google" 94 | 95 | [tool.ruff.lint.flake8-tidy-imports] 96 | ban-relative-imports = "all" 97 | 98 | [tool.ruff.lint.per-file-ignores] 99 | # Tests can use magic values, assertions, and relative imports 100 | "tests/**/*" = ["PLR2004", "S101", "TID252", "ARG001"] 101 | 102 | [tool.pytest.ini_options] 103 | pythonpath = ["src"] 104 | addopts = "--quiet --failed-first -p no:warnings" 105 | testpaths = [ 106 | "tests" 107 | ] 108 | markers = [ 109 | "slow: marks tests as slow", 110 | ] 111 | 112 | [tool.mypy] 113 | python_version = "3.10" 114 | warn_return_any = true 115 | warn_unused_configs = true 116 | files = [ 117 | "django_unicorn_playground" 118 | ] 119 | 120 | [build-system] 121 | requires = ["poetry-core"] 122 | build-backend = "poetry.core.masonry.api" 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-unicorn-playground 🦄🛝 2 | 3 | The `Unicorn Playground` provides a way to prototype and debug [`Unicorn`](https://www.django-unicorn.com) components without creating a complete Django application. It can either be run as a standalone script or by installing the library. 4 | 5 | https://github.com/user-attachments/assets/43533156-deba-4cc1-811b-045115054c73 6 | 7 | ## Standalone 8 | 9 | A python script runner, like `pipx`, can create the virtual environment and install dependencies automatically. [Inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) provides the required dependency information for `pipx`. 10 | 11 | ### Create an example component 12 | 13 | 1. Install [`pipx`](https://pipx.pypa.io/latest/installation/) 14 | 2. Create a new file called `counter.py` with the following code: 15 | 16 | ```python 17 | # /// script 18 | # requires-python = ">=3.10" 19 | # dependencies = [ 20 | # "django-unicorn-playground" 21 | # ] 22 | # /// 23 | 24 | from django_unicorn.components import UnicornView 25 | 26 | class CounterView(UnicornView): 27 | template_html = """
28 |
29 | Count: {{ count }} 30 |
31 | 32 | 33 | 34 |
35 | """ 36 | 37 | count: int 38 | 39 | def increment(self): 40 | count += 1 41 | 42 | def decrement(self): 43 | count -= 1 44 | 45 | if __name__ == "__main__": 46 | from django_unicorn_playground import UnicornPlayground 47 | 48 | UnicornPlayground(__file__).runserver() 49 | ``` 50 | 51 | 3. `pipx run counter.py` 52 | 4. Go to https://localhost:8000 53 | 54 | ### Python script runners 55 | 56 | `pipx` is officially supported, but some other script runners should also work. 57 | 58 | #### [pip-run](https://github.com/jaraco/pip-run) 59 | 60 | `pip-run counter.py` 61 | 62 | #### [fades](https://github.com/PyAr/fades) 63 | 64 | `fades -d django-unicorn-playground counter.py` 65 | 66 | ## CLI 67 | 68 | The `django-unicorn-playground` library can also be installed and provides a command-line interface. This approach doesn't require the script inline metadata or the check for `__name__` -- the CLI takes care of running the development server directly. 69 | 70 | ### Create an example component 71 | 72 | 1. `pipx install django-unicorn-playground` 73 | 2. Create `counter.py` with the following code: 74 | 75 | ```python 76 | from django_unicorn.components import UnicornView 77 | 78 | class CounterView(UnicornView): 79 | template_html = """
80 |
81 | Count: {{ count }} 82 |
83 | 84 | 85 | 86 |
87 | """ 88 | 89 | count: int 90 | 91 | def increment(self): 92 | count += 1 93 | 94 | def decrement(self): 95 | count -= 1 96 | ``` 97 | 98 | 3. `unicorn counter.py` 99 | 4. Go to https://localhost:8000 100 | 101 | ### Arguments 102 | 103 | Screenshot 2024-07-27 at 3 35 06 PM 104 | 105 | #### component 106 | 107 | The path to the component. Required. 108 | 109 | #### --port 110 | 111 | Port for the developer webserver. Required to be an integer. Defaults to 8000. 112 | 113 | #### --template_dir 114 | 115 | Directory for templates. Required to be a string path. 116 | 117 | #### --version 118 | 119 | Shows the current version. 120 | 121 | #### --help 122 | 123 | Show the available CLI options. 124 | 125 | ## Example components 126 | 127 | There are a few example components in the `examples` directory. They can be run with something like `pipx run --no-cache examples/counter.py`. 128 | 129 | ## Template HTML 130 | 131 | ### UnicornView.template_html attribute 132 | 133 | The HTML can be set with a class-level `template_html` field. 134 | 135 | ```python 136 | from django_unicorn.components import UnicornView 137 | 138 | class TestView(UnicornView): 139 | template_html = """
140 |
141 | Count: {{ count }} 142 |
143 | 144 | 145 | 146 |
147 | """ 148 | 149 | ... 150 | ``` 151 | 152 | ### UnicornView.template_html(self) 153 | 154 | The HTML can be returned from a `template_html` instance method. 155 | 156 | ```python 157 | from django_unicorn.components import UnicornView 158 | 159 | class TestView(UnicornView): 160 | def template_html(self): 161 | return """
162 |
163 | Count: {{ count }} 164 |
165 | 166 | 167 | 168 |
169 | """ 170 | 171 | ... 172 | ``` 173 | 174 | ### HTML file 175 | 176 | Similar to a typical `django-unicorn` setup, the component HTML can be in a separate template file. This file will be looked for if `template_html` is not defined on the component. 177 | 178 | 1. `cd` to the same directory as the component Python file you created 179 | 2. `mkdir -p templates/unicorn` 180 | 3. `touch {COMPONENT-NAME}.html`, e.g. for a component Python named `counter.py` create `counter.html` 181 | 4. Add the component HTML to the newly created file 182 | 183 | ### Template directory 184 | 185 | By default `django-unicorn-playground` will look in the `templates/unicorn` folder for templates. The template location can be changed by passing in a `template_dir` kwarg into the `UnicornPlayground()` constructor or using a `--template_dir` argument to the CLI. 186 | 187 | ### index.html 188 | 189 | The root URL dynamically creates `index.html` which creates HTML for the component. It looks something like the following. 190 | 191 | ```html 192 | {% extends "base.html" %} 193 | {% block content %} 194 | {% unicorn 'COMPONENT-NAME' %} 195 | {% endblock content %} 196 | ``` 197 | 198 | It can be overridden by creating a custom `index.html` in the template directory. 199 | 200 | ### base.html 201 | 202 | By default, `index.html` extends [`base.html`](https://github.com/adamghill/django-unicorn-playground/blob/main/src/django_unicorn_playground/templates/base.html). It looks something like the following. 203 | 204 | ```html 205 | 206 | 207 | 208 | {% unicorn_scripts %} 209 | 210 | 211 | {% block content %} 212 | {% endblock content %} 213 | 214 | 215 | ``` 216 | 217 | It can be overridden by creating a custom `base.html` in the template directory. 218 | 219 | ## Using a Python HTML builder 220 | 221 | Any Python library that generates HTML strings works great with `django-unicorn-playground`. 222 | 223 | ### [haitch](https://pypi.org/project/haitch/) 224 | 225 | ```python 226 | from django_unicorn.components import UnicornView 227 | import haitch 228 | 229 | class TestComponent(UnicornView) 230 | def template_html(self): 231 | return haitch.div()( 232 | haitch.button("Increment +", **{"unicorn:click": "increment"}), 233 | haitch.button("Decrement -", **{"unicorn:click": "decrement"}), 234 | haitch.div("{{ count }}"), 235 | ) 236 | 237 | ... 238 | ``` 239 | 240 | ### [htpy](https://pypi.org/project/htpy/) 241 | 242 | ```python 243 | from django_unicorn.components import UnicornView 244 | import htpy 245 | 246 | class TestComponent(UnicornView) 247 | def template_html(self): 248 | return htpy.div()[ 249 | htpy.button({"unicorn:click": "increment"})["Increment +"], 250 | htpy.button({"unicorn:click": "decrement"})["Decrement -"], 251 | htpy.div()["{{ count }}"], 252 | ] 253 | 254 | ... 255 | ``` 256 | 257 | ### [dominate](https://pypi.org/project/dominate/) 258 | 259 | ```python 260 | from django_unicorn.components import UnicornView 261 | from dominate import tags as dom 262 | 263 | class TestComponent(UnicornView) 264 | def template_html(self): 265 | return dom.div( 266 | dom.button("Increment +", **unicorn.click(self.increment)), 267 | dom.button("Decrement -", **unicorn.click(self.decrement)), 268 | dom.div("{{ count }}"), 269 | ) 270 | 271 | ... 272 | ``` 273 | 274 | ## Unicorn HTML helpers 275 | 276 | When using an HTML builder, `django-unicorn-playground` provides a few helper methods which make it a little cleaner to create `Unicorn`-specific HTML. 277 | 278 | For example, if using `haitch` instead of doing this: 279 | 280 | ```python 281 | import haitch 282 | from django_unicorn.components import UnicornView 283 | 284 | class TestComponent(UnicornView) 285 | def template_html(self): 286 | return haitch.div()( 287 | haitch.button("Increment +", **{"unicorn:click": "increment"}), 288 | haitch.button("Decrement -", **{"unicorn:click": "decrement"}), 289 | haitch.div("{{ count }}"), 290 | ) 291 | 292 | ... 293 | ``` 294 | 295 | This is how it would work with the helper methods: 296 | 297 | ```python 298 | import haitch 299 | from django_unicorn.components import UnicornView 300 | from django_unicorn_playground.html import django, unicorn 301 | 302 | class TestComponent(UnicornView) 303 | def template_html(self): 304 | return haitch.div()( 305 | haitch.button("Increment +", **unicorn.click(self.increment)), 306 | haitch.button("Decrement -", **unicorn.click(self.decrement)), 307 | haitch.div(django.variable("count")), 308 | ) 309 | 310 | ... 311 | ``` 312 | 313 | ## Local development 314 | 315 | ### Standalone 316 | 317 | Using the inline script metadata with `pipx` seems a little quirky and I could not get editable installs working reliably. I also tried `hatch run` which had its own issues. Not sure if there are other (read: better) approaches. 318 | 319 | As far as I can tell, the best approach is to use an absolute file path like `"django_unicorn_playground @ file:///Users/adam/Source/adamghill/django-unicorn-playground/dist/django_unicorn_playground-0.1.0-py3-none-any.whl"` (note the triple forward-slash after "file:") as a dependency, and rebuilding and re-running the script without any caching like this: `poetry build && pipx run --no-cache examples/counter.py` any time you make a code change. 320 | 321 | Note: you will need to update the component's dependency so it points to the path and version on your machine. 322 | 323 | There is a `just` command to make testing a standalone script _slightly_ less painful during local development. 324 | 325 | 1. [Install just](https://just.systems/man/en/chapter_4.html) 326 | 1. `just serve {COMPONENT-FILE-PATH}`, e.g. `just serve examples/counter.py` 327 | 328 | ### CLI 329 | 330 | 1. `poetry install` 331 | 1. `poetry run unicorn {COMPONENT-FILE-PATH}`, e.g. `poetry run unicorn examples/counter.py` 332 | 333 | ## Acknowledgments 334 | 335 | - [phoenix_playground](https://github.com/phoenix-playground/phoenix_playground) 336 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.8.1" 6 | description = "ASGI specs, helper code, and adapters" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, 11 | {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 16 | 17 | [package.extras] 18 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 19 | 20 | [[package]] 21 | name = "beautifulsoup4" 22 | version = "4.12.3" 23 | description = "Screen-scraping library" 24 | optional = false 25 | python-versions = ">=3.6.0" 26 | files = [ 27 | {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, 28 | {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, 29 | ] 30 | 31 | [package.dependencies] 32 | soupsieve = ">1.2" 33 | 34 | [package.extras] 35 | cchardet = ["cchardet"] 36 | chardet = ["chardet"] 37 | charset-normalizer = ["charset-normalizer"] 38 | html5lib = ["html5lib"] 39 | lxml = ["lxml"] 40 | 41 | [[package]] 42 | name = "cachetools" 43 | version = "5.4.0" 44 | description = "Extensible memoizing collections and decorators" 45 | optional = false 46 | python-versions = ">=3.7" 47 | files = [ 48 | {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, 49 | {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, 50 | ] 51 | 52 | [[package]] 53 | name = "click" 54 | version = "8.1.7" 55 | description = "Composable command line interface toolkit" 56 | optional = false 57 | python-versions = ">=3.7" 58 | files = [ 59 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 60 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 61 | ] 62 | 63 | [package.dependencies] 64 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 65 | 66 | [[package]] 67 | name = "colorama" 68 | version = "0.4.6" 69 | description = "Cross-platform colored terminal text." 70 | optional = false 71 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 72 | files = [ 73 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 74 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 75 | ] 76 | 77 | [[package]] 78 | name = "coverage" 79 | version = "7.6.0" 80 | description = "Code coverage measurement for Python" 81 | optional = false 82 | python-versions = ">=3.8" 83 | files = [ 84 | {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, 85 | {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, 86 | {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, 87 | {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, 88 | {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, 89 | {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, 90 | {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, 91 | {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, 92 | {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, 93 | {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, 94 | {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, 95 | {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, 96 | {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, 97 | {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, 98 | {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, 99 | {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, 100 | {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, 101 | {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, 102 | {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, 103 | {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, 104 | {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, 105 | {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, 106 | {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, 107 | {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, 108 | {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, 109 | {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, 110 | {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, 111 | {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, 112 | {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, 113 | {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, 114 | {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, 115 | {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, 116 | {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, 117 | {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, 118 | {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, 119 | {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, 120 | {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, 121 | {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, 122 | {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, 123 | {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, 124 | {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, 125 | {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, 126 | {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, 127 | {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, 128 | {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, 129 | {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, 130 | {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, 131 | {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, 132 | {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, 133 | {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, 134 | {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, 135 | {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, 136 | ] 137 | 138 | [package.dependencies] 139 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 140 | 141 | [package.extras] 142 | toml = ["tomli"] 143 | 144 | [[package]] 145 | name = "decorator" 146 | version = "5.1.1" 147 | description = "Decorators for Humans" 148 | optional = false 149 | python-versions = ">=3.5" 150 | files = [ 151 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 152 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 153 | ] 154 | 155 | [[package]] 156 | name = "django" 157 | version = "5.0.7" 158 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 159 | optional = false 160 | python-versions = ">=3.10" 161 | files = [ 162 | {file = "Django-5.0.7-py3-none-any.whl", hash = "sha256:f216510ace3de5de01329463a315a629f33480e893a9024fc93d8c32c22913da"}, 163 | {file = "Django-5.0.7.tar.gz", hash = "sha256:bd4505cae0b9bd642313e8fb71810893df5dc2ffcacaa67a33af2d5cd61888f2"}, 164 | ] 165 | 166 | [package.dependencies] 167 | asgiref = ">=3.7.0,<4" 168 | sqlparse = ">=0.3.1" 169 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 170 | 171 | [package.extras] 172 | argon2 = ["argon2-cffi (>=19.1.0)"] 173 | bcrypt = ["bcrypt"] 174 | 175 | [[package]] 176 | name = "django-stubs" 177 | version = "5.0.2" 178 | description = "Mypy stubs for Django" 179 | optional = false 180 | python-versions = ">=3.8" 181 | files = [ 182 | {file = "django_stubs-5.0.2-py3-none-any.whl", hash = "sha256:cb0c506cb5c54c64612e4a2ee8d6b913c6178560ec168009fe847c09747c304b"}, 183 | {file = "django_stubs-5.0.2.tar.gz", hash = "sha256:236bc5606e5607cb968f92b648471f9edaa461a774bc013bf9e6bff8730f6bdf"}, 184 | ] 185 | 186 | [package.dependencies] 187 | asgiref = "*" 188 | django = "*" 189 | django-stubs-ext = ">=5.0.2" 190 | tomli = {version = "*", markers = "python_version < \"3.11\""} 191 | types-PyYAML = "*" 192 | typing-extensions = ">=4.11.0" 193 | 194 | [package.extras] 195 | compatible-mypy = ["mypy (>=1.10.0,<1.11.0)"] 196 | oracle = ["oracledb"] 197 | redis = ["redis"] 198 | 199 | [[package]] 200 | name = "django-stubs-ext" 201 | version = "5.0.2" 202 | description = "Monkey-patching and extensions for django-stubs" 203 | optional = false 204 | python-versions = ">=3.8" 205 | files = [ 206 | {file = "django_stubs_ext-5.0.2-py3-none-any.whl", hash = "sha256:8d8efec5a86241266bec94a528fe21258ad90d78c67307f3ae5f36e81de97f12"}, 207 | {file = "django_stubs_ext-5.0.2.tar.gz", hash = "sha256:409c62585d7f996cef5c760e6e27ea3ff29f961c943747e67519c837422cad32"}, 208 | ] 209 | 210 | [package.dependencies] 211 | django = "*" 212 | typing-extensions = "*" 213 | 214 | [[package]] 215 | name = "django-unicorn" 216 | version = "0.61.0" 217 | description = "A magical full-stack framework for Django." 218 | optional = false 219 | python-versions = "<4,>=3.8" 220 | files = [ 221 | {file = "django_unicorn-0.61.0-py3-none-any.whl", hash = "sha256:e65adb256a5aed8b3634af4296ddb70ea9fa3ca9682a3cd7e4fa5e79a8bd88a4"}, 222 | {file = "django_unicorn-0.61.0.tar.gz", hash = "sha256:dd3ef5c610785bb69a770926cef4096993e236a1b080e695d8e2ee3aa5f4f603"}, 223 | ] 224 | 225 | [package.dependencies] 226 | beautifulsoup4 = ">=4.8.0" 227 | cachetools = ">=4.1.1" 228 | decorator = ">=4.4.2" 229 | django = ">=2.2" 230 | orjson = ">=3.6.0" 231 | shortuuid = ">=1.0.1" 232 | 233 | [package.extras] 234 | docs = ["Sphinx", "furo", "linkify-it-py", "myst-parser", "rst2pdf", "sphinx-autoapi", "sphinx-autobuild", "sphinx-copybutton", "sphinx-design", "sphinxext-opengraph", "toml"] 235 | minify = ["htmlmin (>=0,<1)"] 236 | 237 | [[package]] 238 | name = "exceptiongroup" 239 | version = "1.2.2" 240 | description = "Backport of PEP 654 (exception groups)" 241 | optional = false 242 | python-versions = ">=3.7" 243 | files = [ 244 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 245 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 246 | ] 247 | 248 | [package.extras] 249 | test = ["pytest (>=6)"] 250 | 251 | [[package]] 252 | name = "iniconfig" 253 | version = "2.0.0" 254 | description = "brain-dead simple config-ini parsing" 255 | optional = false 256 | python-versions = ">=3.7" 257 | files = [ 258 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 259 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 260 | ] 261 | 262 | [[package]] 263 | name = "markdown-it-py" 264 | version = "3.0.0" 265 | description = "Python port of markdown-it. Markdown parsing, done right!" 266 | optional = false 267 | python-versions = ">=3.8" 268 | files = [ 269 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 270 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 271 | ] 272 | 273 | [package.dependencies] 274 | mdurl = ">=0.1,<1.0" 275 | 276 | [package.extras] 277 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 278 | code-style = ["pre-commit (>=3.0,<4.0)"] 279 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 280 | linkify = ["linkify-it-py (>=1,<3)"] 281 | plugins = ["mdit-py-plugins"] 282 | profiling = ["gprof2dot"] 283 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 284 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 285 | 286 | [[package]] 287 | name = "mdurl" 288 | version = "0.1.2" 289 | description = "Markdown URL utilities" 290 | optional = false 291 | python-versions = ">=3.7" 292 | files = [ 293 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 294 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 295 | ] 296 | 297 | [[package]] 298 | name = "orjson" 299 | version = "3.10.6" 300 | description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" 301 | optional = false 302 | python-versions = ">=3.8" 303 | files = [ 304 | {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, 305 | {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, 306 | {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, 307 | {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, 308 | {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, 309 | {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, 310 | {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, 311 | {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, 312 | {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, 313 | {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, 314 | {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, 315 | {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, 316 | {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, 317 | {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, 318 | {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, 319 | {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, 320 | {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, 321 | {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, 322 | {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, 323 | {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, 324 | {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, 325 | {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, 326 | {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, 327 | {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, 328 | {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, 329 | {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, 330 | {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, 331 | {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, 332 | {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, 333 | {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, 334 | {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, 335 | {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, 336 | {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, 337 | {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, 338 | {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, 339 | {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, 340 | {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, 341 | {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, 342 | {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, 343 | {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, 344 | {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, 345 | {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, 346 | {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, 347 | {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, 348 | {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, 349 | {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, 350 | {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, 351 | {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, 352 | {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, 353 | {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, 354 | {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, 355 | ] 356 | 357 | [[package]] 358 | name = "packaging" 359 | version = "24.1" 360 | description = "Core utilities for Python packages" 361 | optional = false 362 | python-versions = ">=3.8" 363 | files = [ 364 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 365 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 366 | ] 367 | 368 | [[package]] 369 | name = "pluggy" 370 | version = "1.5.0" 371 | description = "plugin and hook calling mechanisms for python" 372 | optional = false 373 | python-versions = ">=3.8" 374 | files = [ 375 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 376 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 377 | ] 378 | 379 | [package.extras] 380 | dev = ["pre-commit", "tox"] 381 | testing = ["pytest", "pytest-benchmark"] 382 | 383 | [[package]] 384 | name = "pygments" 385 | version = "2.18.0" 386 | description = "Pygments is a syntax highlighting package written in Python." 387 | optional = false 388 | python-versions = ">=3.8" 389 | files = [ 390 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 391 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 392 | ] 393 | 394 | [package.extras] 395 | windows-terminal = ["colorama (>=0.4.6)"] 396 | 397 | [[package]] 398 | name = "pytest" 399 | version = "8.3.2" 400 | description = "pytest: simple powerful testing with Python" 401 | optional = false 402 | python-versions = ">=3.8" 403 | files = [ 404 | {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, 405 | {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, 406 | ] 407 | 408 | [package.dependencies] 409 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 410 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 411 | iniconfig = "*" 412 | packaging = "*" 413 | pluggy = ">=1.5,<2" 414 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 415 | 416 | [package.extras] 417 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 418 | 419 | [[package]] 420 | name = "pytest-cov" 421 | version = "5.0.0" 422 | description = "Pytest plugin for measuring coverage." 423 | optional = false 424 | python-versions = ">=3.8" 425 | files = [ 426 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 427 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 428 | ] 429 | 430 | [package.dependencies] 431 | coverage = {version = ">=5.2.1", extras = ["toml"]} 432 | pytest = ">=4.6" 433 | 434 | [package.extras] 435 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 436 | 437 | [[package]] 438 | name = "pytest-django" 439 | version = "4.8.0" 440 | description = "A Django plugin for pytest." 441 | optional = false 442 | python-versions = ">=3.8" 443 | files = [ 444 | {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, 445 | {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, 446 | ] 447 | 448 | [package.dependencies] 449 | pytest = ">=7.0.0" 450 | 451 | [package.extras] 452 | docs = ["sphinx", "sphinx-rtd-theme"] 453 | testing = ["Django", "django-configurations (>=2.0)"] 454 | 455 | [[package]] 456 | name = "rich" 457 | version = "13.7.1" 458 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 459 | optional = false 460 | python-versions = ">=3.7.0" 461 | files = [ 462 | {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, 463 | {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, 464 | ] 465 | 466 | [package.dependencies] 467 | markdown-it-py = ">=2.2.0" 468 | pygments = ">=2.13.0,<3.0.0" 469 | 470 | [package.extras] 471 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 472 | 473 | [[package]] 474 | name = "rich-click" 475 | version = "1.8.3" 476 | description = "Format click help output nicely with rich" 477 | optional = false 478 | python-versions = ">=3.7" 479 | files = [ 480 | {file = "rich_click-1.8.3-py3-none-any.whl", hash = "sha256:636d9c040d31c5eee242201b5bf4f2d358bfae4db14bb22ec1cafa717cfd02cd"}, 481 | {file = "rich_click-1.8.3.tar.gz", hash = "sha256:6d75bdfa7aa9ed2c467789a0688bc6da23fbe3a143e19aa6ad3f8bac113d2ab3"}, 482 | ] 483 | 484 | [package.dependencies] 485 | click = ">=7" 486 | rich = ">=10.7" 487 | typing-extensions = "*" 488 | 489 | [package.extras] 490 | dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] 491 | docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] 492 | 493 | [[package]] 494 | name = "shortuuid" 495 | version = "1.0.13" 496 | description = "A generator library for concise, unambiguous and URL-safe UUIDs." 497 | optional = false 498 | python-versions = ">=3.6" 499 | files = [ 500 | {file = "shortuuid-1.0.13-py3-none-any.whl", hash = "sha256:a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a"}, 501 | {file = "shortuuid-1.0.13.tar.gz", hash = "sha256:3bb9cf07f606260584b1df46399c0b87dd84773e7b25912b7e391e30797c5e72"}, 502 | ] 503 | 504 | [[package]] 505 | name = "soupsieve" 506 | version = "2.5" 507 | description = "A modern CSS selector implementation for Beautiful Soup." 508 | optional = false 509 | python-versions = ">=3.8" 510 | files = [ 511 | {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, 512 | {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, 513 | ] 514 | 515 | [[package]] 516 | name = "sqlparse" 517 | version = "0.5.1" 518 | description = "A non-validating SQL parser." 519 | optional = false 520 | python-versions = ">=3.8" 521 | files = [ 522 | {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, 523 | {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, 524 | ] 525 | 526 | [package.extras] 527 | dev = ["build", "hatch"] 528 | doc = ["sphinx"] 529 | 530 | [[package]] 531 | name = "tomli" 532 | version = "2.0.1" 533 | description = "A lil' TOML parser" 534 | optional = false 535 | python-versions = ">=3.7" 536 | files = [ 537 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 538 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 539 | ] 540 | 541 | [[package]] 542 | name = "typeguard" 543 | version = "4.3.0" 544 | description = "Run-time type checker for Python" 545 | optional = false 546 | python-versions = ">=3.8" 547 | files = [ 548 | {file = "typeguard-4.3.0-py3-none-any.whl", hash = "sha256:4d24c5b39a117f8a895b9da7a9b3114f04eb63bade45a4492de49b175b6f7dfa"}, 549 | {file = "typeguard-4.3.0.tar.gz", hash = "sha256:92ee6a0aec9135181eae6067ebd617fd9de8d75d714fb548728a4933b1dea651"}, 550 | ] 551 | 552 | [package.dependencies] 553 | typing-extensions = ">=4.10.0" 554 | 555 | [package.extras] 556 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)"] 557 | test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] 558 | 559 | [[package]] 560 | name = "types-pyyaml" 561 | version = "6.0.12.20240724" 562 | description = "Typing stubs for PyYAML" 563 | optional = false 564 | python-versions = ">=3.8" 565 | files = [ 566 | {file = "types-PyYAML-6.0.12.20240724.tar.gz", hash = "sha256:cf7b31ae67e0c5b2919c703d2affc415485099d3fe6666a6912f040fd05cb67f"}, 567 | {file = "types_PyYAML-6.0.12.20240724-py3-none-any.whl", hash = "sha256:e5becec598f3aa3a2ddf671de4a75fa1c6856fbf73b2840286c9d50fae2d5d48"}, 568 | ] 569 | 570 | [[package]] 571 | name = "typing-extensions" 572 | version = "4.12.2" 573 | description = "Backported and Experimental Type Hints for Python 3.8+" 574 | optional = false 575 | python-versions = ">=3.8" 576 | files = [ 577 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 578 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 579 | ] 580 | 581 | [[package]] 582 | name = "tzdata" 583 | version = "2024.1" 584 | description = "Provider of IANA time zone data" 585 | optional = false 586 | python-versions = ">=2" 587 | files = [ 588 | {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, 589 | {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, 590 | ] 591 | 592 | [metadata] 593 | lock-version = "2.0" 594 | python-versions = "^3.10" 595 | content-hash = "fdd39129af2350770fbd6e720fce2e6aa81ce66bc4d06cc4243148e960fdb7b4" 596 | --------------------------------------------------------------------------------