265 | ['"]? # start quote
266 | [^"']*
267 | ['"]? # end quote
268 | )
269 | """,
270 | re.VERBOSE | re.UNICODE,
271 | )
272 |
273 |
274 | @register.tag("merge_attrs")
275 | def do_merge_attrs(parser: Parser, token: Token):
276 | tag_name, *remaining_bits = token.split_contents()
277 | if not remaining_bits:
278 | raise TemplateSyntaxError("'%s' tag takes at least one argument, the attributes" % tag_name)
279 |
280 | attributes = parser.compile_filter(remaining_bits[0])
281 | attr_list = remaining_bits[1:]
282 |
283 | default_attrs = []
284 | append_attrs = []
285 | for pair in attr_list:
286 | match = attribute_re.match(pair)
287 | if not match:
288 | raise TemplateSyntaxError(
289 | "Malformed arguments to '%s' tag. You must pass the attributes in the form attr=\"value\"." % tag_name
290 | )
291 | dct = match.groupdict()
292 | attr, sign, value = (
293 | dct["attr"],
294 | dct["sign"],
295 | parser.compile_filter(dct["value"]),
296 | )
297 | if sign == "=":
298 | default_attrs.append((attr, value))
299 | elif sign == "+=":
300 | append_attrs.append((attr, value))
301 | else:
302 | raise TemplateSyntaxError("Unknown sign '%s' for attribute '%s'" % (sign, attr))
303 |
304 | return MergeAttrsNode(attributes, default_attrs, append_attrs)
305 |
306 |
307 | class MergeAttrsNode(template.Node):
308 | def __init__(self, attributes, default_attrs, append_attrs):
309 | self.attributes = attributes
310 | self.default_attrs = default_attrs
311 | self.append_attrs = append_attrs
312 |
313 | def render(self, context):
314 | bound_attributes: dict = self.attributes.resolve(context)
315 |
316 | default_attrs = {key: value.resolve(context) for key, value in self.default_attrs}
317 |
318 | append_attrs = {key: value.resolve(context) for key, value in self.append_attrs}
319 |
320 | attrs = merge_attributes(
321 | default_attrs,
322 | bound_attributes,
323 | )
324 | attrs = append_attributes(attrs, append_attrs)
325 |
326 | return attributes_to_string(attrs)
327 |
--------------------------------------------------------------------------------
/tests/test_component.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ImproperlyConfigured
2 | from django.template import Context, Template, NodeList
3 | from django.template.base import TextNode
4 | from django.test import TestCase
5 |
6 | import django_web_components.attributes
7 | from django_web_components import component
8 | from django_web_components.component import (
9 | Component,
10 | )
11 | from django_web_components.templatetags.components import SlotNodeList, SlotNode
12 |
13 |
14 | class ComponentTest(TestCase):
15 | def test_get_template_name_returns_template_name(self):
16 | class DummyComponent(Component):
17 | template_name = "foo.html"
18 |
19 | self.assertEqual(DummyComponent().get_template_name(), "foo.html")
20 |
21 | def test_get_template_name_raises_if_no_template_name_set(self):
22 | class DummyComponent(Component):
23 | pass
24 |
25 | with self.assertRaises(ImproperlyConfigured):
26 | DummyComponent().get_template_name()
27 |
28 | def test_renders_template(self):
29 | class DummyComponent(Component):
30 | template_name = "simple_template.html"
31 |
32 | self.assertHTMLEqual(
33 | DummyComponent().render(
34 | Context(
35 | {
36 | "message": "world",
37 | }
38 | )
39 | ),
40 | """Hello, world!
""",
41 | )
42 |
43 |
44 | class ExampleComponentsTest(TestCase):
45 | def setUp(self) -> None:
46 | component.registry.clear()
47 |
48 | def test_component_with_inline_tag(self):
49 | @component.register("hello")
50 | def dummy(context):
51 | return Template("""Hello, world!
""").render(context)
52 |
53 | self.assertHTMLEqual(
54 | Template(
55 | """
56 | {% #hello %}
57 | """
58 | ).render(Context({})),
59 | """
60 | Hello, world!
61 | """,
62 | )
63 |
64 | def test_component_with_inline_tag_and_attributes(self):
65 | @component.register("hello")
66 | def dummy(context):
67 | return Template("""Hello, world!
""").render(context)
68 |
69 | self.assertHTMLEqual(
70 | Template(
71 | """
72 | {% #hello class="foo" x-on:click='bar' @click="baz" foo:bar.baz="foo" required %}
73 | """
74 | ).render(Context({})),
75 | """
76 | Hello, world!
77 | """,
78 | )
79 |
80 | def test_simple_component(self):
81 | @component.register("hello")
82 | def dummy(context):
83 | return Template("""Hello, world!
""").render(context)
84 |
85 | self.assertHTMLEqual(
86 | Template(
87 | """
88 | {% hello %}{% endhello %}
89 | """
90 | ).render(Context({})),
91 | """
92 | Hello, world!
93 | """,
94 | )
95 |
96 | def test_component_with_name_with_colon(self):
97 | @component.register("hello:foo")
98 | def dummy(context):
99 | return Template("""Hello, world!
""").render(context)
100 |
101 | self.assertHTMLEqual(
102 | Template(
103 | """
104 | {% hello:foo %}{% endhello:foo %}
105 | """
106 | ).render(Context({})),
107 | """
108 | Hello, world!
109 | """,
110 | )
111 |
112 | def test_component_with_name_with_dot(self):
113 | @component.register("hello.foo")
114 | def dummy(context):
115 | return Template("""Hello, world!
""").render(context)
116 |
117 | self.assertHTMLEqual(
118 | Template(
119 | """
120 | {% hello.foo %}{% endhello.foo %}
121 | """
122 | ).render(Context({})),
123 | """
124 | Hello, world!
125 | """,
126 | )
127 |
128 | def test_component_with_context_passed_in(self):
129 | @component.register("hello")
130 | def dummy(context):
131 | return Template(
132 | """
133 | {{ message }}
134 | """
135 | ).render(context)
136 |
137 | self.assertHTMLEqual(
138 | Template(
139 | """
140 | {% with message="hello" %}
141 | {% hello message="hello" %}{% endhello %}
142 | {% endwith %}
143 | """
144 | ).render(Context({})),
145 | """
146 | hello
147 | """,
148 | )
149 |
150 | # Attributes
151 |
152 | def test_component_with_attributes(self):
153 | @component.register("hello")
154 | def dummy(context):
155 | return Template("""Hello, world!
""").render(context)
156 |
157 | self.assertHTMLEqual(
158 | Template(
159 | """
160 | {% hello class="foo" x-on:click='bar' @click="baz" foo:bar.baz="foo" required %}{% endhello %}
161 | """
162 | ).render(Context({})),
163 | """
164 | Hello, world!
165 | """,
166 | )
167 |
168 | def test_component_with_empty_attributes(self):
169 | @component.register("hello")
170 | def dummy(context):
171 | return Template("""Hello, world!
""").render(context)
172 |
173 | self.assertHTMLEqual(
174 | Template(
175 | """
176 | {% hello class="" limit='' required %}{% endhello %}
177 | """
178 | ).render(Context({})),
179 | """
180 | Hello, world!
181 | """,
182 | )
183 |
184 | def test_attributes_from_context_variables(self):
185 | @component.register("hello")
186 | def dummy(context):
187 | return Template(
188 | """
189 |
190 | {% render_slot slots.inner_block %}
191 |
192 | """
193 | ).render(context)
194 |
195 | self.assertHTMLEqual(
196 | Template(
197 | """
198 | {% with object_id="123" message="Hello" %}
199 | {% hello id=object_id %}
200 | {{ message }}
201 | {% endhello %}
202 | {% endwith %}
203 | """
204 | ).render(Context({})),
205 | """
206 |
209 | """,
210 | )
211 |
212 | def test_attributes_with_defaults(self):
213 | @component.register("hello")
214 | def dummy(context):
215 | return Template(
216 | """
217 |
218 | """
219 | ).render(context)
220 |
221 | self.assertHTMLEqual(
222 | Template(
223 | """
224 | {% hello id="123" class="some-class" %}{% endhello %}
225 | """
226 | ).render(Context({})),
227 | """
228 |
229 | """,
230 | )
231 |
232 | # Slots
233 |
234 | def test_component_with_default_slot(self):
235 | @component.register("hello")
236 | def dummy(context):
237 | return Template(
238 | """
239 | {% render_slot slots.inner_block %}
240 | """
241 | ).render(context)
242 |
243 | self.assertHTMLEqual(
244 | Template(
245 | """
246 | {% hello %}Hello{% endhello %}
247 | """
248 | ).render(Context({})),
249 | """
250 | Hello
251 | """,
252 | )
253 |
254 | def test_component_with_named_slot(self):
255 | @component.register("hello")
256 | def dummy(context):
257 | return Template(
258 | """
259 |
260 |
{% render_slot slots.title %}
261 |
{% render_slot slots.inner_block %}
262 |
263 | """
264 | ).render(context)
265 |
266 | self.assertHTMLEqual(
267 | Template(
268 | """
269 | {% hello %}
270 | {% slot title %}Title{% endslot %}
271 | Default slot
272 | {% endhello %}
273 | """
274 | ).render(Context({})),
275 | """
276 |
277 |
Title
278 |
Default slot
279 |
280 | """,
281 | )
282 |
283 | def test_component_with_multiple_slots(self):
284 | @component.register("hello")
285 | def dummy(context):
286 | return Template(
287 | """
288 |
289 |
{% render_slot slots.title %}
290 |
{% render_slot slots.body %}
291 |
{% render_slot slots.inner_block %}
292 |
293 | """
294 | ).render(context)
295 |
296 | self.assertHTMLEqual(
297 | Template(
298 | """
299 | {% hello %}
300 | {% slot title %}Title{% endslot %}
301 | {% slot body %}Body{% endslot %}
302 | Hello
303 | {% endhello %}
304 | """
305 | ).render(Context({})),
306 | """
307 |
308 |
Title
309 |
Body
310 |
Hello
311 |
312 | """,
313 | )
314 |
315 | def test_component_with_duplicate_slot(self):
316 | @component.register("hello")
317 | def dummy(context):
318 | return Template(
319 | """
320 |
321 | {% for row in slots.row %}
322 | {% render_slot row %}
323 | {% endfor %}
324 |
325 | """
326 | ).render(context)
327 |
328 | self.assertHTMLEqual(
329 | Template(
330 | """
331 | {% hello %}
332 | {% slot row %}Row 1{% endslot %}
333 | {% slot row %}Row 2{% endslot %}
334 | {% slot row %}Row 3{% endslot %}
335 | {% endhello %}
336 | """
337 | ).render(Context({})),
338 | """
339 |
340 | Row 1
341 | Row 2
342 | Row 3
343 |
344 | """,
345 | )
346 |
347 | def test_component_with_duplicate_slot_without_for_loop(self):
348 | @component.register("hello")
349 | def dummy(context):
350 | return Template(
351 | """
352 |
353 | {% render_slot slots.row %}
354 |
355 | """
356 | ).render(context)
357 |
358 | self.assertHTMLEqual(
359 | Template(
360 | """
361 | {% hello %}
362 | {% slot row %}
363 | Row 1
364 | {% endslot %}
365 | {% slot row %}
366 | Row 2
367 | {% endslot %}
368 | {% slot row %}
369 | Row 3
370 | {% endslot %}
371 | {% endhello %}
372 | """
373 | ).render(Context({})),
374 | """
375 |
376 | Row 1
377 | Row 2
378 | Row 3
379 |
380 | """,
381 | )
382 |
383 | def test_component_with_duplicate_slot_but_only_one_passed_in(self):
384 | @component.register("hello")
385 | def dummy(context):
386 | return Template(
387 | """
388 |
389 | {% for row in slots.row %}
390 | {% render_slot row %}
391 | {% endfor %}
392 |
393 | """
394 | ).render(context)
395 |
396 | self.assertHTMLEqual(
397 | Template(
398 | """
399 | {% hello %}
400 | {% slot row %}Row 1{% endslot %}
401 | {% endhello %}
402 | """
403 | ).render(Context({})),
404 | """
405 |
408 | """,
409 | )
410 |
411 | # Slot attributes
412 |
413 | def test_slots_with_attributes(self):
414 | @component.register("hello")
415 | def dummy(context):
416 | return Template(
417 | """
418 |
419 |
{% render_slot slots.title %}
420 |
{% render_slot slots.body %}
421 |
422 | """
423 | ).render(context)
424 |
425 | self.assertHTMLEqual(
426 | Template(
427 | """
428 | {% hello id="123" %}
429 | {% slot title class="title" %}Title{% endslot %}
430 | {% slot body class="foo" x-on:click='bar' @click="baz" foo:bar.baz="foo" required %}
431 | Body
432 | {% endslot %}
433 | {% endhello %}
434 | """
435 | ).render(Context({})),
436 | """
437 |
438 |
Title
439 |
Body
440 |
441 | """,
442 | )
443 |
444 | # Scoped slots
445 |
446 | def test_can_render_scoped_slots(self):
447 | @component.register("table")
448 | def table(context):
449 | return Template(
450 | """
451 |
452 |
453 | {% for col in slots.column %}
454 | {{ col.attributes.label }}
455 | {% endfor %}
456 |
457 | {% for row in rows %}
458 |
459 | {% for col in slots.column %}
460 |
461 | {% render_slot col row %}
462 |
463 | {% endfor %}
464 |
465 | {% endfor %}
466 |
467 | """
468 | ).render(context)
469 |
470 | context = Context(
471 | {
472 | "rows": [
473 | {
474 | "name": "John",
475 | "age": 31,
476 | },
477 | {
478 | "name": "Bob",
479 | "age": 51,
480 | },
481 | {
482 | "name": "Alice",
483 | "age": 27,
484 | },
485 | ],
486 | }
487 | )
488 |
489 | # directly accessing the variable
490 | self.assertHTMLEqual(
491 | Template(
492 | """
493 | {% table %}
494 | {% slot column label="Name" %}
495 | {{ row.name }}
496 | {% endslot %}
497 | {% slot column label="Age" %}
498 | {{ row.age }}
499 | {% endslot %}
500 | {% endtable %}
501 | """
502 | ).render(context),
503 | """
504 |
505 |
506 | Name
507 | Age
508 |
509 |
510 | John
511 | 31
512 |
513 |
514 | Bob
515 | 51
516 |
517 |
518 | Alice
519 | 27
520 |
521 |
522 | """,
523 | )
524 |
525 | # using ':let' to define the context variable
526 | self.assertHTMLEqual(
527 | Template(
528 | """
529 | {% table %}
530 | {% slot column :let="user" label="Name" %}
531 | {{ user.name }}
532 | {% endslot %}
533 | {% slot column :let="user" label="Age" %}
534 | {{ user.age }}
535 | {% endslot %}
536 | {% endtable %}
537 | """
538 | ).render(context),
539 | """
540 |
541 |
542 | Name
543 | Age
544 |
545 |
546 | John
547 | 31
548 |
549 |
550 | Bob
551 | 51
552 |
553 |
554 | Alice
555 | 27
556 |
557 |
558 | """,
559 | )
560 |
561 | def test_scoped_slot_works_on_default_slot(self):
562 | @component.register("unordered_list")
563 | def unordered_list(context):
564 | context["entries"] = context["attributes"].pop("entries", [])
565 | return Template(
566 | """
567 |
568 | {% for entry in entries %}
569 |
570 | {% render_slot slots.inner_block entry %}
571 |
572 | {% endfor %}
573 |
574 | """
575 | ).render(context)
576 |
577 | context = Context(
578 | {
579 | "entries": ["apples", "bananas", "cherries"],
580 | }
581 | )
582 |
583 | # directly accessing the variable
584 | self.assertHTMLEqual(
585 | Template(
586 | """
587 | {% unordered_list entries=entries %}
588 | I like {{ entry }}!
589 | {% endunordered_list %}
590 | """
591 | ).render(context),
592 | """
593 |
594 | I like apples!
595 | I like bananas!
596 | I like cherries!
597 |
598 | """,
599 | )
600 |
601 | # using ':let' to define the context variable
602 | self.assertHTMLEqual(
603 | Template(
604 | """
605 | {% unordered_list :let="fruit" entries=entries %}
606 | I like {{ fruit }}!
607 | {% endunordered_list %}
608 | """
609 | ).render(context),
610 | """
611 |
612 | I like apples!
613 | I like bananas!
614 | I like cherries!
615 |
616 | """,
617 | )
618 |
619 | # Nested components
620 |
621 | def test_nested_component(self):
622 | @component.register("hello")
623 | def dummy(context):
624 | return Template(
625 | """
626 | {% render_slot slots.inner_block %}
627 | """
628 | ).render(context)
629 |
630 | self.assertHTMLEqual(
631 | Template(
632 | """
633 | {% hello class="foo" %}
634 | {% hello class="bar" %}
635 | Hello, world!
636 | {% endhello %}
637 | {% endhello %}
638 | """
639 | ).render(Context({})),
640 | """
641 |
642 |
643 | Hello, world!
644 |
645 |
646 | """,
647 | )
648 |
649 | def test_nested_component_with_slots(self):
650 | @component.register("hello")
651 | def dummy(context):
652 | return Template(
653 | """
654 |
655 |
{% render_slot slots.body %}
656 |
657 | """
658 | ).render(context)
659 |
660 | self.assertHTMLEqual(
661 | Template(
662 | """
663 | {% hello class="hello1" %}
664 | {% slot body class="foo" %}
665 | {% hello class="hello2" %}
666 | {% slot body class="bar" %}
667 | Hello, world!
668 | {% endslot %}
669 | {% endhello %}
670 | {% endslot %}
671 | {% endhello %}
672 | """
673 | ).render(Context({})),
674 | """
675 |
676 |
677 |
678 |
679 | Hello, world!
680 |
681 |
682 |
683 |
684 | """,
685 | )
686 |
687 | def test_component_using_other_components(self):
688 | @component.register("header")
689 | def header(context):
690 | return Template(
691 | """
692 |
693 | {% render_slot slots.inner_block %}
694 |
695 | """
696 | ).render(context)
697 |
698 | @component.register("hello")
699 | def dummy(context):
700 | context["title"] = context["attributes"].pop("title", "")
701 | return Template(
702 | """
703 |
704 | {% header %}
705 | {{ title }}
706 | {% endheader %}
707 |
708 |
709 | {% render_slot slots.inner_block %}
710 |
711 |
712 | """
713 | ).render(context)
714 |
715 | self.assertHTMLEqual(
716 | Template(
717 | """
718 | {% hello title="Some title" %}
719 | Hello, world!
720 | {% endhello %}
721 | """
722 | ).render(Context({})),
723 | """
724 |
725 |
726 | Some title
727 |
728 |
729 | Hello, world!
730 |
731 |
732 | """,
733 | )
734 |
735 | # Settings
736 |
737 | def test_can_change_default_slot_name_from_settings(self):
738 | @component.register("hello")
739 | def dummy(context):
740 | return Template(
741 | """
742 | {% render_slot slots.default_slot %}
743 | """
744 | ).render(context)
745 |
746 | with self.settings(
747 | WEB_COMPONENTS={
748 | "DEFAULT_SLOT_NAME": "default_slot",
749 | }
750 | ):
751 | self.assertHTMLEqual(
752 | Template(
753 | """
754 | {% hello %}Hello{% endhello %}
755 | """
756 | ).render(Context({})),
757 | """
758 | Hello
759 | """,
760 | )
761 |
762 |
763 | class RenderComponentTest(TestCase):
764 | def setUp(self) -> None:
765 | component.registry.clear()
766 |
767 | def test_renders_class_component(self):
768 | @component.register("test")
769 | class Dummy(component.Component):
770 | def render(self, context):
771 | return "foo"
772 |
773 | self.assertEqual(
774 | component.render_component(
775 | name="test",
776 | attributes=django_web_components.attributes.AttributeBag(),
777 | slots={},
778 | context=Context({}),
779 | ),
780 | "foo",
781 | )
782 |
783 | def test_passes_context_to_class_component(self):
784 | @component.register("test")
785 | class Dummy(component.Component):
786 | def get_context_data(self, **kwargs) -> dict:
787 | return {
788 | "context_data": "value from get_context_data",
789 | }
790 |
791 | def render(self, context):
792 | return Template(
793 | """
794 |
795 |
{{ context_data }}
796 | {% render_slot slots.inner_block %}
797 |
798 | """
799 | ).render(context)
800 |
801 | self.assertHTMLEqual(
802 | component.render_component(
803 | name="test",
804 | attributes=django_web_components.attributes.AttributeBag(
805 | {
806 | "class": "font-bold",
807 | }
808 | ),
809 | slots={
810 | "inner_block": SlotNodeList(
811 | [
812 | SlotNode(
813 | name="",
814 | unresolved_attributes={},
815 | nodelist=NodeList(
816 | [
817 | TextNode("Hello, world!"),
818 | ]
819 | ),
820 | ),
821 | ]
822 | ),
823 | },
824 | context=Context({}),
825 | ),
826 | """
827 |
828 |
value from get_context_data
829 | Hello, world!
830 |
831 | """,
832 | )
833 |
834 | def test_renders_function_component(self):
835 | @component.register("test")
836 | def dummy(context):
837 | return "foo"
838 |
839 | self.assertEqual(
840 | component.render_component(
841 | name="test",
842 | attributes=django_web_components.attributes.AttributeBag(),
843 | slots={},
844 | context=Context({}),
845 | ),
846 | "foo",
847 | )
848 |
849 | def test_passes_context_to_function_component(self):
850 | @component.register("test")
851 | def dummy(context):
852 | return Template(
853 | """
854 |
855 | {% render_slot slots.inner_block %}
856 |
857 | """
858 | ).render(context)
859 |
860 | self.assertHTMLEqual(
861 | component.render_component(
862 | name="test",
863 | attributes=django_web_components.attributes.AttributeBag(
864 | {
865 | "class": "font-bold",
866 | }
867 | ),
868 | slots={
869 | "inner_block": SlotNodeList(
870 | [
871 | SlotNode(
872 | name="",
873 | unresolved_attributes={},
874 | nodelist=NodeList(
875 | [
876 | TextNode("Hello, world!"),
877 | ]
878 | ),
879 | ),
880 | ]
881 | ),
882 | },
883 | context=Context({}),
884 | ),
885 | """
886 |
887 | Hello, world!
888 |
889 | """,
890 | )
891 |
892 |
893 | class RegisterTest(TestCase):
894 | def setUp(self) -> None:
895 | component.registry.clear()
896 |
897 | def test_called_as_decorator_with_name(self):
898 | @component.register("hello")
899 | def dummy(context):
900 | pass
901 |
902 | self.assertEqual(
903 | component.registry.get("hello"),
904 | dummy,
905 | )
906 |
907 | def test_called_as_decorator_with_no_name(self):
908 | @component.register()
909 | def hello(context):
910 | pass
911 |
912 | self.assertEqual(
913 | component.registry.get("hello"),
914 | hello,
915 | )
916 |
917 | def test_called_as_decorator_with_no_parenthesis(self):
918 | @component.register
919 | def hello(context):
920 | pass
921 |
922 | self.assertEqual(
923 | component.registry.get("hello"),
924 | hello,
925 | )
926 |
927 | def test_called_directly_with_name(self):
928 | def dummy(context):
929 | pass
930 |
931 | component.register("hello", dummy)
932 |
933 | self.assertEqual(
934 | component.registry.get("hello"),
935 | dummy,
936 | )
937 |
938 | def test_called_directly_with_no_name(self):
939 | def hello(context):
940 | pass
941 |
942 | component.register(hello)
943 |
944 | self.assertEqual(
945 | component.registry.get("hello"),
946 | hello,
947 | )
948 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-web-components
2 |
3 | [](https://github.com/Xzya/django-web-components/actions/workflows/tests.yml)
4 | [](https://pypi.org/project/django-web-components/)
5 |
6 | A simple way to create reusable template components in Django.
7 |
8 | ## Example
9 |
10 | You have to first register your component
11 |
12 | ```python
13 | from django_web_components import component
14 |
15 | @component.register("card")
16 | class Card(component.Component):
17 | template_name = "components/card.html"
18 | ```
19 |
20 | The component's template:
21 |
22 | ```html
23 | # components/card.html
24 |
25 | {% load components %}
26 |
27 |
28 |
31 |
32 |
33 | {% render_slot slots.title %}
34 |
35 |
36 | {% render_slot slots.inner_block %}
37 |
38 |
39 | ```
40 |
41 | You can now render this component with:
42 |
43 | ```html
44 | {% load components %}
45 |
46 | {% card %}
47 | {% slot header %} Featured {% endslot %}
48 | {% slot title %} Card title {% endslot %}
49 |
50 | Some quick example text to build on the card title and make up the bulk of the card's content.
51 |
52 | Go somewhere
53 | {% endcard %}
54 | ```
55 |
56 | Which will result in the following HTML being rendered:
57 |
58 | ```html
59 |
60 |
63 |
64 |
65 | Card title
66 |
67 |
68 |
Some quick example text to build on the card title and make up the bulk of the card's content.
69 |
70 |
Go somewhere
71 |
72 |
73 | ```
74 |
75 | ## Installation
76 |
77 | ```
78 | pip install django-web-components
79 | ```
80 |
81 | Then add `django_web_components` to your `INSTALLED_APPS`.
82 |
83 | ```python
84 | INSTALLED_APPS = [
85 | ...,
86 | "django_web_components",
87 | ]
88 | ```
89 |
90 | ### Optional
91 |
92 | To avoid having to use `{% load components %}` in each template, you may add the tags to the `builtins` list inside your
93 | settings.
94 |
95 | ```python
96 | TEMPLATES = [
97 | {
98 | ...,
99 | "OPTIONS": {
100 | "context_processors": [
101 | ...
102 | ],
103 | "builtins": [
104 | "django_web_components.templatetags.components",
105 | ],
106 | },
107 | },
108 | ]
109 | ```
110 |
111 | ## Python / Django compatibility
112 |
113 | The library supports Python 3.8+ and Django 3.2+.
114 |
115 | | Python version | Django version |
116 | |----------------|-----------------------------------|
117 | | `3.12` | `5.0`, `4.2` |
118 | | `3.11` | `5.0`, `4.2`, `4.1` |
119 | | `3.10` | `5.0`, `4.2`, `4.1`, `4.0`, `3.2` |
120 | | `3.9` | `4.2`, `4.1`, `4.0`, `3.2` |
121 | | `3.8` | `4.2`, `4.1`, `4.0`, `3.2` |
122 |
123 | ## Components
124 |
125 | There are two approaches to writing components: class based components and function based components.
126 |
127 | ### Class based components
128 |
129 | ```python
130 | from django_web_components import component
131 |
132 | @component.register("alert")
133 | class Alert(component.Component):
134 | # You may also override the get_template_name() method instead
135 | template_name = "components/alert.html"
136 |
137 | # Extra context data will be passed to the template context
138 | def get_context_data(self, **kwargs) -> dict:
139 | return {
140 | "dismissible": False,
141 | }
142 | ```
143 |
144 | The component will be rendered by calling the `render(context)` method, which by default will load the template file and render it.
145 |
146 | For tiny components, it may feel cumbersome to manage both the component class and the component's template. For this reason, you may define the template directly from the `render` method:
147 |
148 | ```python
149 | from django_web_components import component
150 | from django_web_components.template import CachedTemplate
151 |
152 | @component.register("alert")
153 | class Alert(component.Component):
154 | def render(self, context) -> str:
155 | return CachedTemplate(
156 | """
157 |
158 | {% render_slot slots.inner_block %}
159 |
160 | """,
161 | name="alert",
162 | ).render(context)
163 | ```
164 |
165 | ### Function based components
166 |
167 | A component may also be defined as a single function that accepts a `context` and returns a string:
168 |
169 | ```python
170 | from django_web_components import component
171 | from django_web_components.template import CachedTemplate
172 |
173 | @component.register
174 | def alert(context):
175 | return CachedTemplate(
176 | """
177 |
178 | {% render_slot slots.inner_block %}
179 |
180 | """,
181 | name="alert",
182 | ).render(context)
183 | ```
184 |
185 | The examples in this guide will mostly use function based components, since it's easier to exemplify as the component code and template are in the same place, but you are free to choose whichever method you prefer.
186 |
187 | ### Template files vs template strings
188 |
189 | The library uses the regular Django templates, which allows you to either [load templates from files](https://docs.djangoproject.com/en/dev/ref/templates/api/#loading-templates), or create [Template objects](https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.Template) directly using template strings. Both methods are supported, and both have advantages and disadvantages:
190 |
191 | - Template files
192 | - You get formatting support and syntax highlighting from your editor
193 | - You get [caching](https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.loaders.cached.Loader) by default
194 | - Harder to manage / reason about since your code is split from the template
195 | - Template strings
196 | - Easier to manage / reason about since your component's code and template are in the same place
197 | - You lose formatting support and syntax highlighting since the template is just a string
198 | - You lose caching
199 |
200 | Regarding caching, the library provides a `CachedTemplate`, which will cache and reuse the `Template` object as long as you provide a `name` for it, which will be used as the cache key:
201 |
202 | ```python
203 | from django_web_components import component
204 | from django_web_components.template import CachedTemplate
205 |
206 | @component.register
207 | def alert(context):
208 | return CachedTemplate(
209 | """
210 |
211 | {% render_slot slots.inner_block %}
212 |
213 | """,
214 | name="alert",
215 | ).render(context)
216 | ```
217 |
218 | So in reality, the caching should not be an issue when using template strings, since `CachedTemplate` is just as fast as using the cached loader with template files.
219 |
220 | Regarding formatting support and syntax highlighting, there is no good solution for template strings. PyCharm supports [language injection](https://www.jetbrains.com/help/pycharm/using-language-injections.html#use-language-injection-comments) which allows you to add a `# language=html` comment before the template string and get syntax highlighting, however, it only highlights HTML and not the Django tags, and you are still missing support for formatting. Maybe the editors will add better support for this in the future, but for the moment you will be missing syntax highlighting and formatting if you go this route. There is an [open conversation](https://github.com/EmilStenstrom/django-components/issues/183) about this on the `django-components` repo, credits to [EmilStenstrom](https://github.com/EmilStenstrom) for moving the conversation forward with the VSCode team.
221 |
222 | In the end, it's a tradeoff. Use the method that makes the most sense for you.
223 |
224 | ## Registering your components
225 |
226 | [Just like signals](https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions), the components can live anywhere, but you need to make sure Django picks them up on startup. The easiest way to do this is to define your components in a `components.py` submodule of the application they relate to, and then connect them inside the `ready()` method of your application configuration class.
227 |
228 | ```python
229 | from django.apps import AppConfig
230 | from django_web_components import component
231 |
232 | class MyAppConfig(AppConfig):
233 | ...
234 |
235 | def ready(self):
236 | # Implicitly register components decorated with @component.register
237 | from . import components # noqa
238 | # OR explicitly register a component
239 | component.register("card", components.Card)
240 | ```
241 |
242 | You may also `unregister` an existing component, or get a component from the registry:
243 |
244 | ```python
245 | from django_web_components import component
246 | # Unregister a component
247 | component.registry.unregister("card")
248 | # Get a component
249 | component.registry.get("card")
250 | # Remove all components
251 | component.registry.clear()
252 | # Get all components as a dict of name: component
253 | component.registry.all()
254 | ```
255 |
256 | ## Rendering components
257 |
258 | Each registered component will have two tags available for use in your templates:
259 |
260 | - A block tag, e.g. `{% card %} ... {% endcard %}`
261 | - An inline tag, e.g. `{% #user_profile %}`. This can be useful for components that don't necessarily require a body
262 |
263 | ### Component tag formatter
264 |
265 | By default, components will be registered using the following tags:
266 |
267 | - Block start tag: `{% %}`
268 | - Block end tag: `{% end %}`
269 | - Inline tag: `{% # %}`
270 |
271 | This behavior may be changed by providing a custom tag formatter in your settings. For example, to change the block tags to `{% #card %} ... {% /card %}`, and the inline tag to `{% card %}` (similar to [slippers](https://github.com/mixxorz/slippers)), you can use the following formatter:
272 |
273 | ```python
274 | class ComponentTagFormatter:
275 | def format_block_start_tag(self, name):
276 | return f"#{name}"
277 |
278 | def format_block_end_tag(self, name):
279 | return f"/{name}"
280 |
281 | def format_inline_tag(self, name):
282 | return name
283 |
284 | # inside your settings
285 | WEB_COMPONENTS = {
286 | "DEFAULT_COMPONENT_TAG_FORMATTER": "path.to.your.ComponentTagFormatter",
287 | }
288 | ```
289 |
290 | ## Passing data to components
291 |
292 | You may pass data to components using keyword arguments, which accept either hardcoded values or variables:
293 |
294 | ```html
295 | {% with error_message="Something bad happened!" %}
296 | {% #alert type="error" message=error_message %}
297 | {% endwith %}
298 | ```
299 |
300 | All attributes will be added in an `attributes` dict which will be available in the template context:
301 |
302 | ```json
303 | {
304 | "attributes": {
305 | "type": "error",
306 | "message": "Something bad happened!"
307 | }
308 | }
309 | ```
310 |
311 | You can then access it from your component's template:
312 |
313 | ```html
314 |
315 | {{ attributes.message }}
316 |
317 | ```
318 |
319 | ### Rendering all attributes
320 |
321 | You may also render all attributes directly using `{{ attributes }}`. For example, if you have the following component
322 |
323 | ```html
324 | {% alert id="alerts" class="font-bold" %} ... {% endalert %}
325 | ```
326 |
327 | You may render all attributes using
328 |
329 | ```html
330 |
331 |
332 |
333 | ```
334 |
335 | Which will result in the following HTML being rendered:
336 |
337 | ```html
338 |
339 |
340 |
341 | ```
342 |
343 | ### Attributes with special characters
344 |
345 | You can also pass attributes with special characters (`[@:_-.]`), as well as attributes with no value:
346 |
347 | ```html
348 | {% button @click="handleClick" data-id="123" required %} ... {% endbutton %}
349 | ```
350 |
351 | Which will result in the follow dict available in the context:
352 |
353 | ```python
354 | {
355 | "attributes": {
356 | "@click": "handleClick",
357 | "data-id": "123",
358 | "required": True,
359 | }
360 | }
361 | ```
362 |
363 | And will be rendered by `{{ attributes }}` as `@click="handleClick" data-id="123" required`.
364 |
365 | ### Default / merged attributes
366 |
367 | Sometimes you may need to specify default values for attributes, or merge additional values into some of the component's attributes. The library provides a `merge_attrs` tag that helps with this:
368 |
369 | ```html
370 |
371 |
372 |
373 | ```
374 |
375 | If we assume this component is utilized like so:
376 |
377 | ```html
378 | {% alert class="mb-4" %} ... {% endalert %}
379 | ```
380 |
381 | The final rendered HTML of the component will appear like the following:
382 |
383 | ```html
384 |
385 |
386 |
387 | ```
388 |
389 | ### Non-class attribute merging
390 |
391 | When merging attributes that are not `class` attributes, the values provided to the `merge_attrs` tag will be considered the "default" values of the attribute. However, unlike the `class` attribute, these attributes will not be merged with injected attribute values. Instead, they will be overwritten. For example, a `button` component's implementation may look like the following:
392 |
393 | ```html
394 |
395 | {% render_slot slots.inner_block %}
396 |
397 | ```
398 |
399 | To render the button component with a custom `type`, it may be specified when consuming the component. If no type is specified, the `button` type will be used:
400 |
401 | ```html
402 | {% button type="submit" %} Submit {% endbutton %}
403 | ```
404 |
405 | The rendered HTML of the `button` component in this example would be:
406 |
407 | ```html
408 |
409 | Submit
410 |
411 | ```
412 |
413 | ### Appendable attributes
414 |
415 | You may also treat other attributes as "appendable" by using the `+=` operator:
416 |
417 | ```html
418 |
419 |
420 |
421 | ```
422 |
423 | If we assume this component is utilized like so:
424 |
425 | ```html
426 | {% alert data-value="foo" %} ... {% endalert %}
427 | ```
428 |
429 | The rendered HTML will be:
430 |
431 | ```html
432 |
433 |
434 |
435 | ```
436 |
437 | ### Manipulating the attributes
438 |
439 | By default, all attributes are added to an `attributes` dict inside the context. However, this may not always be what we want. For example, imagine we want to have an `alert` component that can be dismissed, while at the same time being able to pass extra attributes to the root element, like an `id` or `class`. Ideally we would want to be able to render a component like this:
440 |
441 | ```html
442 | {% alert id="alerts" dismissible %} Something went wrong! {% endalert %}
443 | ```
444 |
445 | A naive way to implement this component would be something like the following:
446 |
447 | ```html
448 |
449 | {% render_slot slots.inner_block %}
450 |
451 | {% if attributes.dismissible %}
452 |
453 | {% endif %}
454 |
455 | ```
456 |
457 | However, this would result in the `dismissible` attribute being included in the root element, which is not what we want:
458 |
459 | ```html
460 |
461 | Something went wrong!
462 |
463 |
464 |
465 | ```
466 |
467 | Ideally we would want the `dismissible` attribute to be separated from the `attributes` since we only want to use it in logic, but not necessarily render it to the component.
468 |
469 | To achieve this, you can manipulate the context from your component in order to provide a better API for using the components. There are several ways to do this, choose the method that makes the most sense to you, for example:
470 |
471 | - You can override `get_context_data` and remove the `dismissible` attribute from `attributes` and return it in the context instead
472 |
473 | ```python
474 | from django_web_components import component
475 |
476 | @component.register("alert")
477 | class Alert(component.Component):
478 | template_name = "components/alert.html"
479 |
480 | def get_context_data(self, **kwargs):
481 | dismissible = self.attributes.pop("dismissible", False)
482 |
483 | return {
484 | "dismissible": dismissible,
485 | }
486 | ```
487 |
488 | - You can override the `render` method and manipulate the context there
489 |
490 | ```python
491 | from django_web_components import component
492 |
493 | @component.register("alert")
494 | class Alert(component.Component):
495 | template_name = "components/alert.html"
496 |
497 | def render(self, context):
498 | context["dismissible"] = context["attributes"].pop("dismissible", False)
499 |
500 | return super().render(context)
501 | ```
502 |
503 | Both of the above solutions will work, and you can do the same for function based components. The component's template can then look like this:
504 |
505 | ```html
506 |
507 | {% render_slot slots.inner_block %}
508 |
509 | {% if dismissible %}
510 |
511 | {% endif %}
512 |
513 | ```
514 |
515 | Which should result in the correct HTML being rendered:
516 |
517 | ```html
518 |
519 | Something went wrong!
520 |
521 |
522 |
523 | ```
524 |
525 | ## Slots
526 |
527 | You will often need to pass additional content to your components via "slots". A `slots` context variable is passed to your components, which consists of a dict with the slot name as the key and the slot as the value. You may then render the slots inside your components using the `render_slot` tag.
528 |
529 | ### The default slot
530 |
531 | To explore this concept, let's imagine we want to pass some content to an `alert` component:
532 |
533 | ```html
534 | {% alert %}
535 | Whoops! Something went wrong!
536 | {% endalert %}
537 | ```
538 |
539 | By default, that content will be made available to your component in the default slot which is called `inner_block`. You can then render this slot using the `render_slot` tag inside your component:
540 |
541 | ```html
542 | {% load components %}
543 |
544 | {% render_slot slots.inner_block %}
545 |
546 | ```
547 |
548 | Which should result in the following HTML being rendered:
549 |
550 | ```html
551 |
552 | Whoops! Something went wrong!
553 |
554 | ```
555 |
556 | ---
557 |
558 | You may also rename the default slot by specifying it in your settings:
559 |
560 | ```python
561 | # inside your settings
562 | WEB_COMPONENTS = {
563 | "DEFAULT_SLOT_NAME": "inner_block",
564 | }
565 | ```
566 |
567 | ### Named slots
568 |
569 | Sometimes a component may need to render multiple different slots in different locations within the component. Let's modify our alert component to allow for the injection of a "title" slot:
570 |
571 | ```html
572 | {% load components %}
573 |
574 |
575 | {% render_slot slots.title %}
576 |
577 |
578 | {% render_slot slots.inner_block %}
579 |
580 | ```
581 |
582 | You may define the content of the named slot using the `slot` tag. Any content not within an explicit `slot` tag will be added to the default `inner_block` slot:
583 |
584 | ```html
585 | {% load components %}
586 | {% alert %}
587 | {% slot title %} Server error {% endslot %}
588 |
589 | Whoops! Something went wrong!
590 | {% endalert %}
591 | ```
592 |
593 | The rendered HTML in this example would be:
594 |
595 | ```html
596 |
597 |
598 | Server error
599 |
600 |
601 | Whoops! Something went wrong!
602 |
603 | ```
604 |
605 | ### Duplicate named slots
606 |
607 | You may define the same named slot multiple times:
608 |
609 | ```html
610 | {% unordered_list %}
611 | {% slot item %} First item {% endslot %}
612 | {% slot item %} Second item {% endslot %}
613 | {% slot item %} Third item {% endslot %}
614 | {% endunordered_list %}
615 | ```
616 |
617 | You can then iterate over the slot inside your component:
618 |
619 | ```html
620 |
621 | {% for item in slots.item %}
622 | {% render_slot item %}
623 | {% endfor %}
624 |
625 | ```
626 |
627 | Which will result in the following HTML:
628 |
629 | ```html
630 |
631 | First item
632 | Second item
633 | Third item
634 |
635 | ```
636 |
637 | ### Scoped slots
638 |
639 | The slot content will also have access to the component's context. To explore this concept, imagine a list component that accepts an `entries` attribute representing a list of things, which it will then iterate over and render the `inner_block` slot for each entry.
640 |
641 | ```python
642 | from django_web_components import component
643 | from django_web_components.template import CachedTemplate
644 |
645 | @component.register
646 | def unordered_list(context):
647 | context["entries"] = context["attributes"].pop("entries", [])
648 |
649 | return CachedTemplate(
650 | """
651 |
652 | {% for entry in entries %}
653 |
654 | {% render_slot slots.inner_block %}
655 |
656 | {% endfor %}
657 |
658 | """,
659 | name="unordered_list",
660 | ).render(context)
661 | ```
662 |
663 | We can then render the component as follows:
664 |
665 | ```html
666 | {% unordered_list entries=entries %}
667 | I like {{ entry }}!
668 | {% endunordered_list %}
669 | ```
670 |
671 | In this example, the `entry` variable comes from the component's context. If we assume that `entries = ["apples", "bananas", "cherries"]`, the resulting HTML will be:
672 |
673 | ```html
674 |
675 | I like apples!
676 | I like bananas!
677 | I like cherries!
678 |
679 | ```
680 |
681 | ---
682 |
683 | You may also explicitly pass a second argument to `render_slot`:
684 |
685 | ```html
686 |
687 | {% for entry in entries %}
688 |
689 |
690 | {% render_slot slots.inner_block entry %}
691 |
692 | {% endfor %}
693 |
694 | ```
695 |
696 | When invoking the component, you can use the special attribute `:let` to take the value that was passed to `render_slot` and bind it to a variable:
697 |
698 | ```html
699 | {% unordered_list :let="fruit" entries=entries %}
700 | I like {{ fruit }}!
701 | {% endunordered_list %}
702 | ```
703 |
704 | This would render the same HTML as above.
705 |
706 | ### Slot attributes
707 |
708 | Similar to the components, you may assign additional attributes to slots. Below is a table component illustrating multiple named slots with attributes:
709 |
710 | ```python
711 | from django_web_components import component
712 | from django_web_components.template import CachedTemplate
713 |
714 | @component.register
715 | def table(context):
716 | context["rows"] = context["attributes"].pop("rows", [])
717 |
718 | return CachedTemplate(
719 | """
720 |
721 |
722 | {% for col in slots.column %}
723 | {{ col.attributes.label }}
724 | {% endfor %}
725 |
726 | {% for row in rows %}
727 |
728 | {% for col in slots.column %}
729 |
730 | {% render_slot col row %}
731 |
732 | {% endfor %}
733 |
734 | {% endfor %}
735 |
736 | """,
737 | name="table",
738 | ).render(context)
739 | ```
740 |
741 | You can invoke the component like so:
742 |
743 | ```html
744 | {% table rows=rows %}
745 | {% slot column :let="user" label="Name" %}
746 | {{ user.name }}
747 | {% endslot %}
748 | {% slot column :let="user" label="Age" %}
749 | {{ user.age }}
750 | {% endslot %}
751 | {% endtable %}
752 | ```
753 |
754 | If we assume that `rows = [{ "name": "Jane", "age": "34" }, { "name": "Bob", "age": "51" }]`, the following HTML will be rendered:
755 |
756 | ```html
757 |
758 |
759 | Name
760 | Age
761 |
762 |
763 | Jane
764 | 34
765 |
766 |
767 | Bob
768 | 51
769 |
770 |
771 | ```
772 |
773 | ### Nested components
774 |
775 | You may also nest components to achieve more complicated elements. Here is an example of how you might implement an [Accordion component using Bootstrap](https://getbootstrap.com/docs/5.3/components/accordion/):
776 |
777 | ```python
778 | from django_web_components import component
779 | from django_web_components.template import CachedTemplate
780 | import uuid
781 |
782 | @component.register
783 | def accordion(context):
784 | context["accordion_id"] = context["attributes"].pop("id", str(uuid.uuid4()))
785 | context["always_open"] = context["attributes"].pop("always_open", False)
786 |
787 | return CachedTemplate(
788 | """
789 |
790 | {% render_slot slots.inner_block %}
791 |
792 | """,
793 | name="accordion",
794 | ).render(context)
795 |
796 |
797 | @component.register
798 | def accordion_item(context):
799 | context["id"] = context["attributes"].pop("id", str(uuid.uuid4()))
800 | context["open"] = context["attributes"].pop("open", False)
801 |
802 | return CachedTemplate(
803 | """
804 |
805 |
817 |
825 |
826 | {% render_slot slots.body %}
827 |
828 |
829 |
830 | """,
831 | name="accordion_item",
832 | ).render(context)
833 | ```
834 |
835 | You can then use them as follows:
836 |
837 | ```html
838 | {% accordion %}
839 |
840 | {% accordion_item open %}
841 | {% slot title %} Accordion Item #1 {% endslot %}
842 | {% slot body %}
843 | This is the first item's accordion body. It is shown by default.
844 | {% endslot %}
845 | {% endaccordion_item %}
846 |
847 | {% accordion_item %}
848 | {% slot title %} Accordion Item #2 {% endslot %}
849 | {% slot body %}
850 | This is the second item's accordion body. It is hidden by default.
851 | {% endslot %}
852 | {% endaccordion_item %}
853 |
854 | {% accordion_item %}
855 | {% slot title %} Accordion Item #3 {% endslot %}
856 | {% slot body %}
857 | This is the third item's accordion body. It is hidden by default.
858 | {% endslot %}
859 | {% endaccordion_item %}
860 |
861 | {% endaccordion %}
862 | ```
863 |
864 | ## Setup for development and running the tests
865 |
866 | The project uses `poetry` to manage the dependencies. Check out the documentation on how to install poetry here: https://python-poetry.org/docs/#installation
867 |
868 | Install the dependencies
869 |
870 | ```bash
871 | poetry install
872 | ```
873 |
874 | Activate the environment
875 |
876 | ```bash
877 | poetry shell
878 | ```
879 |
880 | Now you can run the tests
881 |
882 | ```bash
883 | python runtests.py
884 | ```
885 |
886 | ## Motivation / Inspiration / Resources
887 |
888 | The project came to be after seeing how other languages / frameworks deal with components, and wanting to bring some of those ideas back to Django.
889 |
890 | - [django-components](https://github.com/EmilStenstrom/django-components) - The existing `django-components` library is already great and supports most of the features that this project has, but I thought the syntax could be improved a bit to feel less verbose, and add a few extra things that seemed necessary, like support for function based components and template strings, and working with attributes
891 | - [Phoenix Components](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) - I really liked the simplicity of Phoenix and how they deal with components, and this project is heavily inspired by it. In fact, some of the examples above are straight-up copied from there (like the table example).
892 | - [Laravel Blade Components](https://laravel.com/docs/9.x/blade#components) - The initial implementation was actually very different and was relying on HTML parsing to turn the HTML into template Nodes, and was heavily inspired by Laravel. This had the benefit of having a nicer syntax (e.g. rendering the components looked a lot like normal HTML `Server Error `), but the solution was a lot more complicated and I came to the conclusion that using a similar approach to `django-components` made a lot more sense in Django
893 | - [Vue Components](https://vuejs.org/guide/essentials/component-basics.html)
894 | - [slippers](https://github.com/mixxorz/slippers)
895 | - [django-widget-tweaks](https://github.com/jazzband/django-widget-tweaks)
896 | - [How EEx Turns Your Template Into HTML](https://www.mitchellhanberg.com/how-eex-turns-your-template-into-html/)
897 |
898 | ### Component libraries
899 |
900 | Many other languages / frameworks are using the same concepts for building components (slots, attributes), so a lot of the knowledge is transferable, and there is already a great deal of existing component libraries out there (e.g. using Bootstrap, Tailwind, Material design, etc.). I highly recommend looking at some of them to get inspired on how to build / structure your components. Here are some examples:
901 |
902 | - https://bootstrap-vue.org/docs/components/alert
903 | - https://coreui.io/bootstrap-vue/components/alert.html
904 | - https://laravel-bootstrap-components.com/components/alerts
905 | - https://flowbite.com/docs/components/alerts/
906 | - https://www.creative-tim.com/vuematerial/components/button
907 | - https://phoenix-ui.fly.dev/components/alert
908 | - https://storybook.phenixgroupe.com/components/message
909 | - https://surface-ui.org/samplecomponents/Button
910 | - https://react-bootstrap.github.io/components/alerts/
911 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "asgiref"
5 | version = "3.7.2"
6 | description = "ASGI specs, helper code, and adapters"
7 | category = "main"
8 | optional = false
9 | python-versions = ">=3.7"
10 | files = [
11 | {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
12 | {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
13 | ]
14 |
15 | [package.dependencies]
16 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
17 |
18 | [package.extras]
19 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
20 |
21 | [[package]]
22 | name = "backports-zoneinfo"
23 | version = "0.2.1"
24 | description = "Backport of the standard library zoneinfo module"
25 | category = "main"
26 | optional = false
27 | python-versions = ">=3.6"
28 | files = [
29 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
30 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
31 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
32 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
33 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
34 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
35 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
36 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
37 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
38 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
39 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
40 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
41 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
42 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
43 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
44 | {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
45 | ]
46 |
47 | [package.extras]
48 | tzdata = ["tzdata"]
49 |
50 | [[package]]
51 | name = "black"
52 | version = "23.10.0"
53 | description = "The uncompromising code formatter."
54 | category = "dev"
55 | optional = false
56 | python-versions = ">=3.8"
57 | files = [
58 | {file = "black-23.10.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:f8dc7d50d94063cdfd13c82368afd8588bac4ce360e4224ac399e769d6704e98"},
59 | {file = "black-23.10.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:f20ff03f3fdd2fd4460b4f631663813e57dc277e37fb216463f3b907aa5a9bdd"},
60 | {file = "black-23.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3d9129ce05b0829730323bdcb00f928a448a124af5acf90aa94d9aba6969604"},
61 | {file = "black-23.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:960c21555be135c4b37b7018d63d6248bdae8514e5c55b71e994ad37407f45b8"},
62 | {file = "black-23.10.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:30b78ac9b54cf87bcb9910ee3d499d2bc893afd52495066c49d9ee6b21eee06e"},
63 | {file = "black-23.10.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:0e232f24a337fed7a82c1185ae46c56c4a6167fb0fe37411b43e876892c76699"},
64 | {file = "black-23.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31946ec6f9c54ed7ba431c38bc81d758970dd734b96b8e8c2b17a367d7908171"},
65 | {file = "black-23.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:c870bee76ad5f7a5ea7bd01dc646028d05568d33b0b09b7ecfc8ec0da3f3f39c"},
66 | {file = "black-23.10.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:6901631b937acbee93c75537e74f69463adaf34379a04eef32425b88aca88a23"},
67 | {file = "black-23.10.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:481167c60cd3e6b1cb8ef2aac0f76165843a374346aeeaa9d86765fe0dd0318b"},
68 | {file = "black-23.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74892b4b836e5162aa0452393112a574dac85e13902c57dfbaaf388e4eda37c"},
69 | {file = "black-23.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:47c4510f70ec2e8f9135ba490811c071419c115e46f143e4dce2ac45afdcf4c9"},
70 | {file = "black-23.10.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:76baba9281e5e5b230c9b7f83a96daf67a95e919c2dfc240d9e6295eab7b9204"},
71 | {file = "black-23.10.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:a3c2ddb35f71976a4cfeca558848c2f2f89abc86b06e8dd89b5a65c1e6c0f22a"},
72 | {file = "black-23.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db451a3363b1e765c172c3fd86213a4ce63fb8524c938ebd82919bf2a6e28c6a"},
73 | {file = "black-23.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:7fb5fc36bb65160df21498d5a3dd330af8b6401be3f25af60c6ebfe23753f747"},
74 | {file = "black-23.10.0-py3-none-any.whl", hash = "sha256:e223b731a0e025f8ef427dd79d8cd69c167da807f5710add30cdf131f13dd62e"},
75 | {file = "black-23.10.0.tar.gz", hash = "sha256:31b9f87b277a68d0e99d2905edae08807c007973eaa609da5f0c62def6b7c0bd"},
76 | ]
77 |
78 | [package.dependencies]
79 | click = ">=8.0.0"
80 | mypy-extensions = ">=0.4.3"
81 | packaging = ">=22.0"
82 | pathspec = ">=0.9.0"
83 | platformdirs = ">=2"
84 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
85 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
86 |
87 | [package.extras]
88 | colorama = ["colorama (>=0.4.3)"]
89 | d = ["aiohttp (>=3.7.4)"]
90 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
91 | uvloop = ["uvloop (>=0.15.2)"]
92 |
93 | [[package]]
94 | name = "cachetools"
95 | version = "5.3.1"
96 | description = "Extensible memoizing collections and decorators"
97 | category = "dev"
98 | optional = false
99 | python-versions = ">=3.7"
100 | files = [
101 | {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"},
102 | {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"},
103 | ]
104 |
105 | [[package]]
106 | name = "cfgv"
107 | version = "3.4.0"
108 | description = "Validate configuration and produce human readable error messages."
109 | category = "dev"
110 | optional = false
111 | python-versions = ">=3.8"
112 | files = [
113 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
114 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
115 | ]
116 |
117 | [[package]]
118 | name = "chardet"
119 | version = "5.2.0"
120 | description = "Universal encoding detector for Python 3"
121 | category = "dev"
122 | optional = false
123 | python-versions = ">=3.7"
124 | files = [
125 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
126 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
127 | ]
128 |
129 | [[package]]
130 | name = "click"
131 | version = "8.1.7"
132 | description = "Composable command line interface toolkit"
133 | category = "dev"
134 | optional = false
135 | python-versions = ">=3.7"
136 | files = [
137 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
138 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
139 | ]
140 |
141 | [package.dependencies]
142 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
143 |
144 | [[package]]
145 | name = "colorama"
146 | version = "0.4.6"
147 | description = "Cross-platform colored terminal text."
148 | category = "dev"
149 | optional = false
150 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
151 | files = [
152 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
153 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
154 | ]
155 |
156 | [[package]]
157 | name = "coverage"
158 | version = "7.3.2"
159 | description = "Code coverage measurement for Python"
160 | category = "dev"
161 | optional = false
162 | python-versions = ">=3.8"
163 | files = [
164 | {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"},
165 | {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"},
166 | {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"},
167 | {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"},
168 | {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"},
169 | {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"},
170 | {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"},
171 | {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"},
172 | {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"},
173 | {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"},
174 | {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"},
175 | {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"},
176 | {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"},
177 | {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"},
178 | {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"},
179 | {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"},
180 | {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"},
181 | {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"},
182 | {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"},
183 | {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"},
184 | {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"},
185 | {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"},
186 | {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"},
187 | {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"},
188 | {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"},
189 | {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"},
190 | {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"},
191 | {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"},
192 | {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"},
193 | {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"},
194 | {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"},
195 | {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"},
196 | {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"},
197 | {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"},
198 | {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"},
199 | {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"},
200 | {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"},
201 | {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"},
202 | {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"},
203 | {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"},
204 | {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"},
205 | {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"},
206 | {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"},
207 | {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"},
208 | {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"},
209 | {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"},
210 | {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"},
211 | {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"},
212 | {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"},
213 | {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"},
214 | {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"},
215 | {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"},
216 | ]
217 |
218 | [package.extras]
219 | toml = ["tomli"]
220 |
221 | [[package]]
222 | name = "distlib"
223 | version = "0.3.7"
224 | description = "Distribution utilities"
225 | category = "dev"
226 | optional = false
227 | python-versions = "*"
228 | files = [
229 | {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
230 | {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
231 | ]
232 |
233 | [[package]]
234 | name = "django"
235 | version = "4.2.6"
236 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
237 | category = "main"
238 | optional = false
239 | python-versions = ">=3.8"
240 | files = [
241 | {file = "Django-4.2.6-py3-none-any.whl", hash = "sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215"},
242 | {file = "Django-4.2.6.tar.gz", hash = "sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f"},
243 | ]
244 |
245 | [package.dependencies]
246 | asgiref = ">=3.6.0,<4"
247 | "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
248 | sqlparse = ">=0.3.1"
249 | tzdata = {version = "*", markers = "sys_platform == \"win32\""}
250 |
251 | [package.extras]
252 | argon2 = ["argon2-cffi (>=19.1.0)"]
253 | bcrypt = ["bcrypt"]
254 |
255 | [[package]]
256 | name = "filelock"
257 | version = "3.12.4"
258 | description = "A platform independent file lock."
259 | category = "dev"
260 | optional = false
261 | python-versions = ">=3.8"
262 | files = [
263 | {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"},
264 | {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"},
265 | ]
266 |
267 | [package.extras]
268 | docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
269 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
270 | typing = ["typing-extensions (>=4.7.1)"]
271 |
272 | [[package]]
273 | name = "identify"
274 | version = "2.5.30"
275 | description = "File identification library for Python"
276 | category = "dev"
277 | optional = false
278 | python-versions = ">=3.8"
279 | files = [
280 | {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"},
281 | {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"},
282 | ]
283 |
284 | [package.extras]
285 | license = ["ukkonen"]
286 |
287 | [[package]]
288 | name = "mypy-extensions"
289 | version = "1.0.0"
290 | description = "Type system extensions for programs checked with the mypy type checker."
291 | category = "dev"
292 | optional = false
293 | python-versions = ">=3.5"
294 | files = [
295 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
296 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
297 | ]
298 |
299 | [[package]]
300 | name = "nodeenv"
301 | version = "1.8.0"
302 | description = "Node.js virtual environment builder"
303 | category = "dev"
304 | optional = false
305 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
306 | files = [
307 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
308 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
309 | ]
310 |
311 | [package.dependencies]
312 | setuptools = "*"
313 |
314 | [[package]]
315 | name = "packaging"
316 | version = "23.2"
317 | description = "Core utilities for Python packages"
318 | category = "dev"
319 | optional = false
320 | python-versions = ">=3.7"
321 | files = [
322 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
323 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
324 | ]
325 |
326 | [[package]]
327 | name = "pathspec"
328 | version = "0.11.2"
329 | description = "Utility library for gitignore style pattern matching of file paths."
330 | category = "dev"
331 | optional = false
332 | python-versions = ">=3.7"
333 | files = [
334 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
335 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
336 | ]
337 |
338 | [[package]]
339 | name = "platformdirs"
340 | version = "3.11.0"
341 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
342 | category = "dev"
343 | optional = false
344 | python-versions = ">=3.7"
345 | files = [
346 | {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
347 | {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
348 | ]
349 |
350 | [package.extras]
351 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
352 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
353 |
354 | [[package]]
355 | name = "pluggy"
356 | version = "1.3.0"
357 | description = "plugin and hook calling mechanisms for python"
358 | category = "dev"
359 | optional = false
360 | python-versions = ">=3.8"
361 | files = [
362 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
363 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
364 | ]
365 |
366 | [package.extras]
367 | dev = ["pre-commit", "tox"]
368 | testing = ["pytest", "pytest-benchmark"]
369 |
370 | [[package]]
371 | name = "pre-commit"
372 | version = "3.5.0"
373 | description = "A framework for managing and maintaining multi-language pre-commit hooks."
374 | category = "dev"
375 | optional = false
376 | python-versions = ">=3.8"
377 | files = [
378 | {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
379 | {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
380 | ]
381 |
382 | [package.dependencies]
383 | cfgv = ">=2.0.0"
384 | identify = ">=1.0.0"
385 | nodeenv = ">=0.11.1"
386 | pyyaml = ">=5.1"
387 | virtualenv = ">=20.10.0"
388 |
389 | [[package]]
390 | name = "pyproject-api"
391 | version = "1.6.1"
392 | description = "API to interact with the python pyproject.toml based projects"
393 | category = "dev"
394 | optional = false
395 | python-versions = ">=3.8"
396 | files = [
397 | {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"},
398 | {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"},
399 | ]
400 |
401 | [package.dependencies]
402 | packaging = ">=23.1"
403 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
404 |
405 | [package.extras]
406 | docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"]
407 | testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"]
408 |
409 | [[package]]
410 | name = "pyyaml"
411 | version = "6.0.1"
412 | description = "YAML parser and emitter for Python"
413 | category = "dev"
414 | optional = false
415 | python-versions = ">=3.6"
416 | files = [
417 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
418 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
419 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
420 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
421 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
422 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
423 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
424 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
425 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
426 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
427 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
428 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
429 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
430 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
431 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
432 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
433 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
434 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
435 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
436 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
437 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
438 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
439 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
440 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
441 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
442 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
443 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
444 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
445 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
446 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
447 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
448 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
449 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
450 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
451 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
452 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
453 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
454 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
455 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
456 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
457 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
458 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
459 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
460 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
461 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
462 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
463 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
464 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
465 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
466 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
467 | ]
468 |
469 | [[package]]
470 | name = "ruff"
471 | version = "0.1.0"
472 | description = "An extremely fast Python linter, written in Rust."
473 | category = "dev"
474 | optional = false
475 | python-versions = ">=3.7"
476 | files = [
477 | {file = "ruff-0.1.0-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:87114e254dee35e069e1b922d85d4b21a5b61aec759849f393e1dbb308a00439"},
478 | {file = "ruff-0.1.0-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:764f36d2982cc4a703e69fb73a280b7c539fd74b50c9ee531a4e3fe88152f521"},
479 | {file = "ruff-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65f4b7fb539e5cf0f71e9bd74f8ddab74cabdd673c6fb7f17a4dcfd29f126255"},
480 | {file = "ruff-0.1.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:299fff467a0f163baa282266b310589b21400de0a42d8f68553422fa6bf7ee01"},
481 | {file = "ruff-0.1.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d412678bf205787263bb702c984012a4f97e460944c072fd7cfa2bd084857c4"},
482 | {file = "ruff-0.1.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a5391b49b1669b540924640587d8d24128e45be17d1a916b1801d6645e831581"},
483 | {file = "ruff-0.1.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee8cd57f454cdd77bbcf1e11ff4e0046fb6547cac1922cc6e3583ce4b9c326d1"},
484 | {file = "ruff-0.1.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa7aeed7bc23861a2b38319b636737bf11cfa55d2109620b49cf995663d3e888"},
485 | {file = "ruff-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04cd4298b43b16824d9a37800e4c145ba75c29c43ce0d74cad1d66d7ae0a4c5"},
486 | {file = "ruff-0.1.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7186ccf54707801d91e6314a016d1c7895e21d2e4cd614500d55870ed983aa9f"},
487 | {file = "ruff-0.1.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d88adfd93849bc62449518228581d132e2023e30ebd2da097f73059900d8dce3"},
488 | {file = "ruff-0.1.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ad2ccdb3bad5a61013c76a9c1240fdfadf2c7103a2aeebd7bcbbed61f363138f"},
489 | {file = "ruff-0.1.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b77f6cfa72c6eb19b5cac967cc49762ae14d036db033f7d97a72912770fd8e1c"},
490 | {file = "ruff-0.1.0-py3-none-win32.whl", hash = "sha256:480bd704e8af1afe3fd444cc52e3c900b936e6ca0baf4fb0281124330b6ceba2"},
491 | {file = "ruff-0.1.0-py3-none-win_amd64.whl", hash = "sha256:a76ba81860f7ee1f2d5651983f87beb835def94425022dc5f0803108f1b8bfa2"},
492 | {file = "ruff-0.1.0-py3-none-win_arm64.whl", hash = "sha256:45abdbdab22509a2c6052ecf7050b3f5c7d6b7898dc07e82869401b531d46da4"},
493 | {file = "ruff-0.1.0.tar.gz", hash = "sha256:ad6b13824714b19c5f8225871cf532afb994470eecb74631cd3500fe817e6b3f"},
494 | ]
495 |
496 | [[package]]
497 | name = "setuptools"
498 | version = "68.2.2"
499 | description = "Easily download, build, install, upgrade, and uninstall Python packages"
500 | category = "dev"
501 | optional = false
502 | python-versions = ">=3.8"
503 | files = [
504 | {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
505 | {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
506 | ]
507 |
508 | [package.extras]
509 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
510 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
511 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
512 |
513 | [[package]]
514 | name = "sqlparse"
515 | version = "0.4.4"
516 | description = "A non-validating SQL parser."
517 | category = "main"
518 | optional = false
519 | python-versions = ">=3.5"
520 | files = [
521 | {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
522 | {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
523 | ]
524 |
525 | [package.extras]
526 | dev = ["build", "flake8"]
527 | doc = ["sphinx"]
528 | test = ["pytest", "pytest-cov"]
529 |
530 | [[package]]
531 | name = "tomli"
532 | version = "2.0.1"
533 | description = "A lil' TOML parser"
534 | category = "dev"
535 | optional = false
536 | python-versions = ">=3.7"
537 | files = [
538 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
539 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
540 | ]
541 |
542 | [[package]]
543 | name = "tox"
544 | version = "4.11.3"
545 | description = "tox is a generic virtualenv management and test command line tool"
546 | category = "dev"
547 | optional = false
548 | python-versions = ">=3.8"
549 | files = [
550 | {file = "tox-4.11.3-py3-none-any.whl", hash = "sha256:599af5e5bb0cad0148ac1558a0b66f8fff219ef88363483b8d92a81e4246f28f"},
551 | {file = "tox-4.11.3.tar.gz", hash = "sha256:5039f68276461fae6a9452a3b2c7295798f00a0e92edcd9a3b78ba1a73577951"},
552 | ]
553 |
554 | [package.dependencies]
555 | cachetools = ">=5.3.1"
556 | chardet = ">=5.2"
557 | colorama = ">=0.4.6"
558 | filelock = ">=3.12.3"
559 | packaging = ">=23.1"
560 | platformdirs = ">=3.10"
561 | pluggy = ">=1.3"
562 | pyproject-api = ">=1.6.1"
563 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
564 | virtualenv = ">=20.24.3"
565 |
566 | [package.extras]
567 | docs = ["furo (>=2023.8.19)", "sphinx (>=7.2.4)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.24)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
568 | testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.18)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.12)", "wheel (>=0.41.2)"]
569 |
570 | [[package]]
571 | name = "typing-extensions"
572 | version = "4.8.0"
573 | description = "Backported and Experimental Type Hints for Python 3.8+"
574 | category = "main"
575 | optional = false
576 | python-versions = ">=3.8"
577 | files = [
578 | {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
579 | {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
580 | ]
581 |
582 | [[package]]
583 | name = "tzdata"
584 | version = "2023.3"
585 | description = "Provider of IANA time zone data"
586 | category = "main"
587 | optional = false
588 | python-versions = ">=2"
589 | files = [
590 | {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"},
591 | {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"},
592 | ]
593 |
594 | [[package]]
595 | name = "virtualenv"
596 | version = "20.24.5"
597 | description = "Virtual Python Environment builder"
598 | category = "dev"
599 | optional = false
600 | python-versions = ">=3.7"
601 | files = [
602 | {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"},
603 | {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"},
604 | ]
605 |
606 | [package.dependencies]
607 | distlib = ">=0.3.7,<1"
608 | filelock = ">=3.12.2,<4"
609 | platformdirs = ">=3.9.1,<4"
610 |
611 | [package.extras]
612 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
613 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
614 |
615 | [metadata]
616 | lock-version = "2.0"
617 | python-versions = ">=3.8,<4.0"
618 | content-hash = "95af0f6e663a24ef086a789743248a76d0dad0c6828c7c97810a20f62dd2a483"
619 |
--------------------------------------------------------------------------------