mod: add etpgrf typograph

This commit is contained in:
2026-04-10 16:14:57 +03:00
parent 360af67ed3
commit 50067b9bd2
7 changed files with 363 additions and 129 deletions

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from cadpoint.settings import * from bs4 import BeautifulSoup
from html import unescape
import pytils
import re import re
@@ -12,42 +14,56 @@ def check_cookies(request) -> bool:
def safe_html_special_symbols(s: str) -> str: def safe_html_special_symbols(s: str) -> str:
""" Очистка строки от HTML-разметки типографа """Преобразует HTML-фрагмент в чистый текст.
:param s: строка которую надо очистить Удаляет все HTML-теги и декодирует HTML-сущности в Unicode.
:return: str:
:param s: строка, которую надо очистить
:return: str: чистый текст без HTML-разметки
""" """
# очистка строки от некоторых спец-символов HTML if not s:
result = s.replace('­', '­') return ""
result = result.replace('<span class="laquo">', '')
result = result.replace('<span style="margin-right:0.44em;">', '') soup = BeautifulSoup(s, "html.parser")
result = result.replace('<span style="margin-left:-0.44em;">', '')
result = result.replace('<span class="raquo">', '') # Скрипты и стили в чистый текст не нужны — выкидываем их целиком.
result = result.replace('<span class="point">', '') for tag in soup(["script", "style", "noscript", "code", "kbd", "pre"]):
result = result.replace('<span class="thinsp">', ' ') tag.decompose()
result = result.replace('<span class="ensp">', '')
result = result.replace('</span>', '') result = soup.get_text()
result = result.replace('&nbsp;', ' ') result = unescape(result).replace("\xa0", " ")
result = result.replace('&laquo;', '«') # Убираем мягкие переносы и другие невидимые символы, которые не нужны
result = result.replace('&raquo;', '»') # ни для slug, ни для человекочитаемого текста.
result = result.replace('&hellip;', '') result = result.translate({
result = result.replace('<nobr>', '') ord("\xad"): None, # символ мягкого переноса
result = result.replace('</nobr>', '') ord("\u200b"): None, # символ нулевой ширины (zero-width space)
result = result.replace('&mdash;', '') ord("\u200c"): None, # символ нулевой ширины (zero-width non-joiner)
result = result.replace('&#8470;', '') ord("\u200d"): None, # символ Zero Width Joiner (ZWJ)
result = result.replace('<br />', ' ') ord("\u2060"): None, # символ Word Joiner (WJ)
result = result.replace('<br>', ' ') ord("\ufeff"): None, # символ Zero Width No-Break Space (BOM)
return result })
return " ".join(result.split())
def post_processing_html(s: str) -> str: def clean_text_to_slug(s: str, default: str = "content") -> str:
s = re.sub(r"\s+", " ", s, flags=re.IGNORECASE) """Готовит чистый slug из HTML/Unicode текста."""
s = re.sub(r">\s+|>&nbsp;", "> ", s, flags=re.IGNORECASE) slug = pytils.translit.slugify(safe_html_special_symbols(s).lower())
s = re.sub(r"\n|\r|<p[^>]*>\s*</p>|<p>&nbsp;</p>", "", s, flags=re.IGNORECASE) slug = re.sub(r"-+", "-", slug).strip("-")
s = re.sub(r"</p>\s*<br[^>]*>", "</p>", s, flags=re.IGNORECASE) return slug or default
s = re.sub(r"<br[^>]*>\s*<p>|<p[^>]*>\s*<p[^>]*>", "<p>", s, flags=re.IGNORECASE)
s = re.sub(r"</p>\s*</p>", "</p>", s, flags=re.IGNORECASE)
s = re.sub(r"<br[^>]*>\s*<br[^>]*>", "<br />", s, flags=re.IGNORECASE) # Удалить: HTML-постобработка была нужна только для старого типографа Муравьёва.
s = re.sub(r"<p><blockquote>", "<blockquote>", s, flags=re.IGNORECASE) # После перехода на `etpgrf` можно будет убрать и этот закомментированный блок,
s = re.sub(r"</blockquote></p>", "</blockquote>", s, flags=re.IGNORECASE) # и сам импорт `re`, если он больше нигде не понадобится.
return s #
# def post_processing_html(s: str) -> str:
# s = re.sub(r"\s+", " ", s, flags=re.IGNORECASE)
# s = re.sub(r">\s+|>&nbsp;", "> ", s, flags=re.IGNORECASE)
# s = re.sub(r"\n|\r|<p[^>]*>\s*</p>|<p>&nbsp;</p>", "", s, flags=re.IGNORECASE)
# s = re.sub(r"</p>\s*<br[^>]*>", "</p>", s, flags=re.IGNORECASE)
# s = re.sub(r"<br[^>]*>\s*<p>|<p[^>]*>\s*<p[^>]*>", "<p>", s, flags=re.IGNORECASE)
# s = re.sub(r"</p>\s*</p>", "</p>", s, flags=re.IGNORECASE)
# s = re.sub(r"<br[^>]*>\s*<br[^>]*>", "<br />", s, flags=re.IGNORECASE)
# s = re.sub(r"<p><blockquote>", "<blockquote>", s, flags=re.IGNORECASE)
# s = re.sub(r"</blockquote></p>", "</blockquote>", s, flags=re.IGNORECASE)
# return s

View File

@@ -1,15 +1,49 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import logging
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from etpgrf import Hyphenator, Typographer
from filer.fields.image import FilerFileField from filer.fields.image import FilerFileField
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from taggit.models import Tag, TaggedItem from taggit.models import Tag, TaggedItem
from web.add_function import safe_html_special_symbols, post_processing_html from web.add_function import clean_text_to_slug, safe_html_special_symbols
import urllib3
import pytils import pytils
import random
import datetime
logger = logging.getLogger(__name__)
# Типограф настраиваем один раз на модуль: в save() он только обрабатывает строку,
# а не пересоздаётся на каждый объект контента.
_TYPOGRAPHER_LANGS = 'ru+en'
_TYPOGRAPHER_MAX_UNHYPHENATED_LEN = 14
def _build_typographer(hanging_punctuation=None) -> Typographer:
"""Собирает `etpgrf` с едиными настройками для заголовка и текста."""
return Typographer(
langs=_TYPOGRAPHER_LANGS,
process_html=True,
hyphenation=Hyphenator(
langs=_TYPOGRAPHER_LANGS,
max_unhyphenated_len=_TYPOGRAPHER_MAX_UNHYPHENATED_LEN,
),
hanging_punctuation=hanging_punctuation,
)
_TYPOGRAPHER_HEAD = _build_typographer(hanging_punctuation='left')
_TYPOGRAPHER_TEXT = _build_typographer()
def _typograph_text(text: str, typographer: Typographer) -> str:
"""Применяет `etpgrf` к HTML-фрагменту и не валит save при сбое библиотеки."""
if not text:
return text
try:
return typographer.process(text)
except Exception:
logger.exception("etpgrf не смог обработать текст, сохраняем исходный вариант")
return text
# класс для транслитерации русскоязычных slug # класс для транслитерации русскоязычных slug
@@ -113,13 +147,13 @@ class TbContent(models.Model):
) )
bTypograf = models.BooleanField( bTypograf = models.BooleanField(
default=False, default=False,
verbose_name="Типограф Стандарт", verbose_name="Типограф etpgrf",
help_text="Обработать через <a href=\"https://www.typograf.ru\"" help_text="Обработать через <a href=\"https://typograph.cube2.ru/\""
" target=\"_blank\">Типограф 2.0</a><br />" " target=\"_blank\">Типограф ETPRGF</a><br />"
"<small><b>НОРМАЛЬНЫЙ ТИПОГРАФ, ХОРОШИЙ HTML, РЕКОМЕНДУЕМ</b> " "<small><b>СТАБИЛЬНЫЙ И СОВРЕМЕННЫЙ ТИПОГРАФ, РЕКОМЕНДУЕМ</b> "
"&laquo;приклеивает&raquo; союзы, поддерживает неразрывные конструкции, " "&laquo;приклеивает&raquo; союзы и предлоги, поддерживает неразрывные конструкции, "
"замена тире, кавычек и дефисов, расстановка &laquo;мягких переносов&raquo; " "замена тире, кавычек и дефисов, расстановка &laquo;мягких переносов&raquo; "
"в словах длиннее 12 символов, убирает &laquo;вдовы&raquo; &laquo;сироты&raquo; (кроме " "в словах длиннее 14 символов, убирает &laquo;вдовы&raquo; &laquo;сироты&raquo; (кроме "
"заголовков), расставляет абзацы (кроме заголовков), расшифровывает " "заголовков), расставляет абзацы (кроме заголовков), расшифровывает "
"аббревиатуры (те, что знает и кроме заголовков), висячая " "аббревиатуры (те, что знает и кроме заголовков), висячая "
"пунктуация (только в заголовках) и т.п.</small>" "пунктуация (только в заголовках) и т.п.</small>"
@@ -158,87 +192,24 @@ class TbContent(models.Model):
return u"%03d: %s" % (self.id, result[:50] + "" if len(result) > 50 else result) return u"%03d: %s" % (self.id, result[:50] + "" if len(result) > 50 else result)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# переопределяем метод save() чтобы "проверуть" тексты через типографы... # Переопределяем save(), чтобы автоматически типографировать контент перед сохранением.
if self.szContentSlug is None or self.szContentSlug == "" or " " in self.szContentSlug: if self.szContentSlug is None or self.szContentSlug == "" or " " in self.szContentSlug:
# print("ку-ку", self.szContentHead) # print("ку-ку", self.szContentHead)
result_slug = pytils.translit.slugify( base_slug = clean_text_to_slug(self.szContentHead)
safe_html_special_symbols(self.szContentHead)).lower() result_slug = base_slug
while TbContent.objects.filter(szContentSlug=result_slug).count() != 0: suffix = 1
result_slug = "%s-%x" % (result_slug[0: -3], int(random.uniform(0, 255))) while TbContent.objects.filter(szContentSlug=result_slug).exists():
result_slug = f"{base_slug}-{suffix}"
suffix += 1
self.szContentSlug = result_slug self.szContentSlug = result_slug
if self.bTypograf: if self.bTypograf:
# Используем типограф Eugene Spearance (https://www.typograf.ru) через API # `etpgrf` уже умеет HTML-режим и висячую пунктуацию, поэтому здесь
# Настройки стиля типографики см. тут: https://www.typograf.ru/webservice/about/ # не нужен старый локальный fallback.
try: # Для заголовка включаем левую висячую пунктуацию, а для анонса и
http = urllib3.PoolManager() # тела текста оставляем обычную обработку без hanging punctuation.
resp = http.request("POST", "https://www.typograf.ru/webservice/", self.szContentHead = _typograph_text(self.szContentHead, _TYPOGRAPHER_HEAD)
fields={"text": self.szContentHead.encode('cp1251'), self.szContentIntro = _typograph_text(self.szContentIntro, _TYPOGRAPHER_TEXT)
'xml': '<?xml version="1.0" encoding="windows-1251" ?>' self.szContentBody = _typograph_text(self.szContentBody, _TYPOGRAPHER_TEXT)
'<preferences>'
' <!-- Абзацы НЕ СТАВИМ-->'
' <paragraph insert="0" />'
' <!-- Переводы строк НЕ СТАВИМ -->'
' <newline insert="0" />'
' <!-- Неразрывные конструкции ДА -->'
' <hanging-punct insert="1" />'
' <!-- Переносы слов длиннее 12 знаков -->'
' <hyphen insert="1" length="12" />'
'</preferences>'.encode('cp1251')})
result = resp.data.decode('cp1251')
if len(result) <= 512:
self.szContentHead = result
resp = http.request("POST", "https://www.typograf.ru/webservice/",
fields={"text": self.szContentIntro.encode('cp1251'),
'xml': '<?xml version="1.0" encoding="windows-1251" ?>'
'<preferences>'
' <!-- Висячая пунктуация УДАЛЯЕТСЯ -->'
' <hanging-punct insert="1" />'
' <!-- Висячие слова УДАЛЯЕМ -->'
' <hanging-line delete="1" />'
' <!-- Переносы слов длиннее 12 знаков -->'
' <hyphen insert="1" length="12" />'
' <!-- Параметры ссылок -->'
' <link target="_blank" />'
'</preferences>'.encode('cp1251')})
self.szContentIntro = resp.data.decode('cp1251')
resp = http.request("POST", "https://www.typograf.ru/webservice/",
fields={"text": self.szContentBody.encode('cp1251'),
'xml': '<?xml version="1.0" encoding="windows-1251" ?>'
'<preferences>'
' <!-- Висячая пунктуация УДАЛЯЕТСЯ -->'
' <hanging-punct insert="1" />'
' <!-- Висячие слова УДАЛЯЕМ -->'
' <hanging-line delete="1" />'
' <!-- Переносы слов длиннее 10 знаков -->'
' <hyphen insert="1" length="12" />'
' <!-- Параметры ссылок -->'
' <link target="_blank" />'
'</preferences>'.encode('cp1251')})
self.szContentBody = resp.data.decode('cp1251')
except:
# если API типографа не доступен, то подключаем локальный типограф Муравьева
import web.EMT as EMT
emt_header = EMT.EMTypograph()
emt_header.setup({'Text.paragraphs': 'off'})
emt_header.set_text(self.szContentHead)
self.szContentHead = emt_header.apply()
emt_intro = EMT.EMTypograph()
# print("==================================== self.szContentBody\n", self.szContentIntro)
# print("-----------------")
emt_intro.set_text(self.szContentIntro)
# emt_intro.set_tag_layout(layout=EMT.LAYOUT_CLASS)
self.szContentIntro = emt_intro.apply()
self.szContentIntro = post_processing_html(self.szContentIntro)
# print(self.szContentIntro)
emt_body = EMT.EMTypograph()
# print("==================================== self.szContentBody")
# print(self.szContentBody)
# print("-----------------")
emt_body.set_text(self.szContentBody)
# emt_body.set_tag_layout(layout=EMT.LAYOUT_CLASS)
self.szContentBody = emt_body.apply()
self.szContentBody = post_processing_html(self.szContentBody)
# print(self.szContentBody)
self.bTypograf = False self.bTypograf = False
if self.dtContentCreate is None: if self.dtContentCreate is None:
self.dtContentCreate = datetime.datetime.now() self.dtContentCreate = datetime.datetime.now()

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django import template from django import template
from web.add_function import safe_html_special_symbols from web.add_function import clean_text_to_slug, safe_html_special_symbols
import pytils
register = template.Library() register = template.Library()
@@ -16,9 +15,9 @@ def slug_ru(value: str, arg: int) -> str:
""" """
try: try:
arg = int(arg) arg = int(arg)
return pytils.translit.slugify(str(value).lower())[:int(arg)] return clean_text_to_slug(str(value))[:int(arg)]
except ValueError: except ValueError:
return pytils.translit.slugify(str(value).lower()) return clean_text_to_slug(str(value))
@register.filter @register.filter

View File

@@ -1,9 +1,13 @@
from unittest.mock import patch
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase
from django.urls import reverse from django.urls import reverse
from taggit.models import Tag from taggit.models import Tag
from web.add_function import clean_text_to_slug, safe_html_special_symbols
from web.legacy_links import build_canonical_url, replace_legacy_links from web.legacy_links import build_canonical_url, replace_legacy_links
from web.models import TbContent
class LegacyLinksTests(SimpleTestCase): class LegacyLinksTests(SimpleTestCase):
@@ -35,6 +39,23 @@ class LegacyLinksTests(SimpleTestCase):
self.assertEqual(len(matches), 1) self.assertEqual(len(matches), 1)
class SafeHtmlSpecialSymbolsTests(SimpleTestCase):
def test_strips_html_tags_and_decodes_entities(self):
text = '<p>&laquo;Привет&nbsp;<b>мир</b>&raquo; &shy;<script>alert(1)</script><style>p{}</style></p>'
self.assertEqual(safe_html_special_symbols(text), '«Привет мир»')
def test_clean_text_to_slug_normalizes_non_latin_symbols(self):
self.assertEqual(clean_text_to_slug('αβγ ΔΩ'), 'content')
self.assertEqual(clean_text_to_slug('₽ € $ ₴ ₿'), 'content')
def test_tbcontent_str_uses_clean_text(self):
item = TbContent(id=7, szContentHead='<b>&laquo;Привет&nbsp;мир&raquo;</b>')
self.assertEqual(str(item), '007: «Привет мир»')
class TagAutocompleteTests(TestCase): class TagAutocompleteTests(TestCase):
def setUp(self): def setUp(self):
user_model = get_user_model() user_model = get_user_model()
@@ -85,3 +106,40 @@ class TagAutocompleteTests(TestCase):
self.assertEqual(payload['pagination']['more'], False) self.assertEqual(payload['pagination']['more'], False)
class TypographTests(TestCase):
def test_save_generates_slug_from_clean_text(self):
item = TbContent(szContentHead='<b>Привет&nbsp;мир</b>')
item.save()
self.assertEqual(item.szContentSlug, 'privet-mir')
def test_save_normalizes_non_latin_slug_to_default(self):
item = TbContent(szContentHead='αβγ ΔΩ')
item.save()
self.assertEqual(item.szContentSlug, 'content')
def test_save_uses_etpgrf_and_clears_flag(self):
item = TbContent(
szContentHead='«Привет»',
szContentIntro='<p>Абзац</p>',
szContentBody='<p>Тело</p>',
bTypograf=True,
)
with patch('web.models._TYPOGRAPHER_HEAD.process') as head_process_mock, \
patch('web.models._TYPOGRAPHER_TEXT.process') as text_process_mock:
head_process_mock.side_effect = lambda text: f'HEAD[{text}]'
text_process_mock.side_effect = lambda text: f'TEXT[{text}]'
item.save()
self.assertEqual(head_process_mock.call_count, 1)
self.assertEqual(text_process_mock.call_count, 2)
self.assertEqual(item.szContentHead, 'HEAD[«Привет»]')
self.assertEqual(item.szContentIntro, 'TEXT[<p>Абзац</p>]')
self.assertEqual(item.szContentBody, 'TEXT[<p>Тело</p>]')
self.assertFalse(item.bTypograf)

174
poetry.lock generated
View File

@@ -14,6 +14,28 @@ files = [
[package.extras] [package.extras]
tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"]
[[package]]
name = "beautifulsoup4"
version = "4.14.3"
description = "Screen-scraping library"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"},
{file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"},
]
[package.dependencies]
soupsieve = ">=1.6.1"
typing-extensions = ">=4.0.0"
[package.extras]
cchardet = ["cchardet"]
chardet = ["chardet"]
charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.7" version = "3.4.7"
@@ -354,6 +376,22 @@ svglib = {version = "*", optional = true, markers = "extra == \"svg\""}
[package.extras] [package.extras]
svg = ["reportlab", "svglib"] svg = ["reportlab", "svglib"]
[[package]]
name = "etpgrf"
version = "0.1.6.post1"
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
optional = false
python-versions = ">=3.10"
files = [
{file = "etpgrf-0.1.6.post1-py3-none-any.whl", hash = "sha256:0863b14385bdacdd405f137ca2ce6bdb6f683f0189e8c927196a1eee754366be"},
{file = "etpgrf-0.1.6.post1.tar.gz", hash = "sha256:984d201cff232a58c05b6f4455a50f822162520df829ad4d543bfe0b7fd962a9"},
]
[package.dependencies]
beautifulsoup4 = ">=4.10.0"
lxml = ">=4.9.0"
regex = ">=2022.1.18"
[[package]] [[package]]
name = "lxml" name = "lxml"
version = "6.0.2" version = "6.0.2"
@@ -628,6 +666,129 @@ files = [
{file = "pytils-0.4.4.tar.gz", hash = "sha256:9992a96caad57daa211584df1da4fd825f11e836d3ed93011785f1d02ab6f0ca"}, {file = "pytils-0.4.4.tar.gz", hash = "sha256:9992a96caad57daa211584df1da4fd825f11e836d3ed93011785f1d02ab6f0ca"},
] ]
[[package]]
name = "regex"
version = "2026.4.4"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.10"
files = [
{file = "regex-2026.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74fa82dcc8143386c7c0392e18032009d1db715c25f4ba22d23dc2e04d02a20f"},
{file = "regex-2026.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a85b620a388d6c9caa12189233109e236b3da3deffe4ff11b84ae84e218a274f"},
{file = "regex-2026.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2895506ebe32cc63eeed8f80e6eae453171cfccccab35b70dc3129abec35a5b8"},
{file = "regex-2026.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6780f008ee81381c737634e75c24e5a6569cc883c4f8e37a37917ee79efcafd9"},
{file = "regex-2026.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88e9b048345c613f253bea4645b2fe7e579782b82cac99b1daad81e29cc2ed8e"},
{file = "regex-2026.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be061028481186ba62a0f4c5f1cc1e3d5ab8bce70c89236ebe01023883bc903b"},
{file = "regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2228c02b368d69b724c36e96d3d1da721561fb9cc7faa373d7bf65e07d75cb5"},
{file = "regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0540e5b733618a2f84e9cb3e812c8afa82e151ca8e19cf6c4e95c5a65198236f"},
{file = "regex-2026.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf9b1b2e692d4877880388934ac746c99552ce6bf40792a767fd42c8c99f136d"},
{file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:011bb48bffc1b46553ac704c975b3348717f4e4aa7a67522b51906f99da1820c"},
{file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8512fcdb43f1bf18582698a478b5ab73f9c1667a5b7548761329ef410cd0a760"},
{file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:867bddc63109a0276f5a31999e4c8e0eb7bbbad7d6166e28d969a2c1afeb97f9"},
{file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1b9a00b83f3a40e09859c78920571dcb83293c8004079653dd22ec14bbfa98c7"},
{file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e355be718caf838aa089870259cf1776dc2a4aa980514af9d02c59544d9a8b22"},
{file = "regex-2026.4.4-cp310-cp310-win32.whl", hash = "sha256:33bfda9684646d323414df7abe5692c61d297dbb0530b28ec66442e768813c59"},
{file = "regex-2026.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:0709f22a56798457ae317bcce42aacee33c680068a8f14097430d9f9ba364bee"},
{file = "regex-2026.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:ee9627de8587c1a22201cb16d0296ab92b4df5cdcb5349f4e9744d61db7c7c98"},
{file = "regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6"},
{file = "regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87"},
{file = "regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8"},
{file = "regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada"},
{file = "regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d"},
{file = "regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87"},
{file = "regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4"},
{file = "regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86"},
{file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59"},
{file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453"},
{file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80"},
{file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b"},
{file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f"},
{file = "regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351"},
{file = "regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735"},
{file = "regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54"},
{file = "regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52"},
{file = "regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb"},
{file = "regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76"},
{file = "regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be"},
{file = "regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1"},
{file = "regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13"},
{file = "regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9"},
{file = "regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d"},
{file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3"},
{file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0"},
{file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043"},
{file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244"},
{file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73"},
{file = "regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f"},
{file = "regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b"},
{file = "regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983"},
{file = "regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943"},
{file = "regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031"},
{file = "regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7"},
{file = "regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17"},
{file = "regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17"},
{file = "regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae"},
{file = "regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e"},
{file = "regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d"},
{file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27"},
{file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf"},
{file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0"},
{file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa"},
{file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b"},
{file = "regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62"},
{file = "regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81"},
{file = "regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427"},
{file = "regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c"},
{file = "regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141"},
{file = "regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717"},
{file = "regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07"},
{file = "regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca"},
{file = "regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520"},
{file = "regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883"},
{file = "regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b"},
{file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1"},
{file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b"},
{file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff"},
{file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb"},
{file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4"},
{file = "regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa"},
{file = "regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0"},
{file = "regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe"},
{file = "regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7"},
{file = "regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752"},
{file = "regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951"},
{file = "regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f"},
{file = "regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8"},
{file = "regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4"},
{file = "regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9"},
{file = "regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83"},
{file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb"},
{file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465"},
{file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4"},
{file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566"},
{file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95"},
{file = "regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8"},
{file = "regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4"},
{file = "regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f"},
{file = "regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3"},
{file = "regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e"},
{file = "regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6"},
{file = "regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359"},
{file = "regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a"},
{file = "regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55"},
{file = "regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99"},
{file = "regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790"},
{file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc"},
{file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f"},
{file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863"},
{file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a"},
{file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81"},
{file = "regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74"},
{file = "regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45"},
{file = "regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d"},
{file = "regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423"},
]
[[package]] [[package]]
name = "reportlab" name = "reportlab"
version = "4.4.10" version = "4.4.10"
@@ -670,6 +831,17 @@ enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.18.*)", "pytest-mypy"] type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.18.*)", "pytest-mypy"]
[[package]]
name = "soupsieve"
version = "2.8.3"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
python-versions = ">=3.9"
files = [
{file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"},
{file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"},
]
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.5" version = "0.5.5"
@@ -772,4 +944,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.12,<3.13" python-versions = ">=3.12,<3.13"
content-hash = "8115895b95f66275813106c93c64640813e5939a984e76ad53a17032f0788de0" content-hash = "8284fc2ef5f2a06d27b41da40cc2067920b8fd5fed8f23621b777a15d8ca4559"

View File

@@ -403,4 +403,21 @@ nav > .pagination > .page-item > .page-link {
} }
nav > .pagination > .page-item > .page-link:hover {background: #008DD2; transition: 0.6s;} nav > .pagination > .page-item > .page-link:hover {background: #008DD2; transition: 0.6s;}
nav > .pagination > .page-item > .page-link:not(hover) {transition: 1.2s;} nav > .pagination > .page-item > .page-link:not(hover) {transition: 1.2s;}
nav > .pagination > .page-item.disabled > .page-link {color: silver; background: gray} nav > .pagination > .page-item.disabled > .page-link {color: silver; background: gray}
/* -----------------------------------------
СТИЛИ ДЛЯ ВИСЯЧЕЙ ПУНКТУАЦИИ ТИПОГРАФА ETPGRF
Значения отступов (padding) для компенсирующих пробелов и полей (margin) для самих символов висячей
пунктуации приведены для шрифта Times New Roman и должны быть скорректированы в зависимости
от выбранного вами шрифта.
------------------------------------------ */
/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ --- */
.etp-laquo { margin-left: -0.49em; } /* « */
.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ “ */
.etp-lsquo { margin-left: -0.22em; } /* */
.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.23em; } /* ( [ { */
/* компенсирующие пробелы для левых висячих символов */
.etp-sp-laquo { padding-right: 0.49em; }
.etp-sp-ldquo, .etp-sp-bdquo { padding-right: 0.4em; }
.etp-sp-lsquo { padding-right: 0.22em; }
.etp-sp-lpar, .etp-sp-lsqb, .etp-sp-lcub { padding-right: 0.35em; }

View File

@@ -23,6 +23,7 @@ django-environ = "^0.13"
django-mptt = "^0.18.0" django-mptt = "^0.18.0"
pytils = "^0.4.4" pytils = "^0.4.4"
django-select2 = "^8.4.8" django-select2 = "^8.4.8"
etpgrf = "^0.1.6"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
django-debug-toolbar = "^6.3" django-debug-toolbar = "^6.3"