├── 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 |
Increment +
14 |
Decrement -
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 |
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 |
--------------------------------------------------------------------------------