From e7230ba63c8e639c50bffb57c5f0d9348dc0ca65 Mon Sep 17 00:00:00 2001
From: Untone {community.created_by.name || community.created_by.email}
+
+
{community.stat.shouts}
{community.stat.followers}
{community.stat.authors}
diff --git a/panel/routes/invites.tsx b/panel/routes/invites.tsx
index 2ef1dbf0..f1ef1b05 100644
--- a/panel/routes/invites.tsx
+++ b/panel/routes/invites.tsx
@@ -233,7 +233,7 @@ const InvitesRoute: Component
Hello world
+ + + """ + result = extract_text(html) + assert "Hello world" in result, f"Результат: {result}" + + def test_extract_text_from_html_complex(self): + """Тест extract_text с комплексным HTML""" + from utils.extract_text import extract_text + html = """ + + + +Paragraph with bold text
+Test
" + result = wrap_html_fragment(fragment) + assert "" in result + assert "" in result + assert "Test
" in result + + def test_wrap_html_fragment_full_html(self): + """Тест wrap_html_fragment с полным HTML""" + from utils.extract_text import wrap_html_fragment + full_html = "Test
" + result = wrap_html_fragment(full_html) + assert result == full_html + + +class TestUtilsGenerateSlug: + """Тесты для utils.generate_slug""" + + def test_generate_slug_import(self): + """Тест импорта generate_slug""" + from utils.generate_slug import replace_translit, generate_unique_slug + assert replace_translit is not None + assert generate_unique_slug is not None + + def test_replace_translit_simple(self): + """Тест replace_translit с простым текстом""" + from utils.generate_slug import replace_translit + result = replace_translit("hello") + assert result == "hello" + + def test_replace_translit_with_special_chars(self): + """Тест replace_translit со специальными символами""" + from utils.generate_slug import replace_translit + result = replace_translit("hello.world") + assert result == "hello-world" + + def test_replace_translit_with_cyrillic(self): + """Тест replace_translit с кириллицей""" + from utils.generate_slug import replace_translit + result = replace_translit("привет") + assert result == "privet" # Корректная транслитерация слова "привет" + + def test_replace_translit_empty(self): + """Тест replace_translit с пустой строкой""" + from utils.generate_slug import replace_translit + result = replace_translit("") + assert result == "" + + def test_replace_translit_none(self): + """Тест replace_translit с None""" + from utils.generate_slug import replace_translit + result = replace_translit(None) + assert result == "" + + def test_replace_translit_with_numbers(self): + """Тест replace_translit с числами""" + from utils.generate_slug import replace_translit + result = replace_translit("test123") + assert "123" in result + + def test_replace_translit_multiple_spaces(self): + """Тест replace_translit с множественными пробелами""" + from utils.generate_slug import replace_translit + result = replace_translit("hello world") + assert "hello" in result + assert "world" in result + + @patch('utils.generate_slug.local_session') + def test_generate_unique_slug(self, mock_session): + """Тест generate_unique_slug с моком сессии""" + from utils.generate_slug import generate_unique_slug + mock_session.return_value.__enter__.return_value.query.return_value.where.return_value.first.return_value = None + result = generate_unique_slug("test") + assert isinstance(result, str) + assert len(result) > 0 diff --git a/utils/diff.py b/utils/diff.py index 41160d16..b7fc278e 100644 --- a/utils/diff.py +++ b/utils/diff.py @@ -13,7 +13,8 @@ def get_diff(original: str, modified: str) -> list[str]: Returns: A list of differences. """ - return list(ndiff(original.split(), modified.split())) + diff = list(ndiff(original.split(), modified.split())) + return [d for d in diff if d.startswith(("+", "-"))] def apply_diff(original: str, diff: list[str]) -> str: diff --git a/utils/encoders.py b/utils/encoders.py index 5212f57a..4df6184e 100644 --- a/utils/encoders.py +++ b/utils/encoders.py @@ -2,8 +2,8 @@ JSON encoders and utilities """ -import datetime -import decimal +import json +from datetime import date, datetime from typing import Any, Union import orjson @@ -11,56 +11,76 @@ import orjson def default_json_encoder(obj: Any) -> Any: """ - Default JSON encoder для объектов, которые не поддерживаются стандартным JSON + Кастомный JSON энкодер для сериализации нестандартных типов. Args: obj: Объект для сериализации Returns: - Сериализуемое представление объекта + Сериализованное представление объекта Raises: TypeError: Если объект не может быть сериализован """ - if hasattr(obj, "dict") and callable(obj.dict): - return obj.dict() - if hasattr(obj, "__dict__"): - return obj.__dict__ - if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)): + # Обработка datetime + if isinstance(obj, (datetime, date)): return obj.isoformat() - if isinstance(obj, decimal.Decimal): - return float(obj) + + serialized = False + + # Обработка объектов с методом __json__ if hasattr(obj, "__json__"): - return obj.__json__() - msg = f"Object of type {type(obj)} is not JSON serializable" - raise TypeError(msg) + try: + result = obj.__json__() + serialized = True + return result + except Exception as _e: + serialized = False + + # Обработка объектов с методом to_dict + if hasattr(obj, "to_dict"): + try: + result = obj.to_dict() + serialized = True + return result + except Exception as _e: + serialized = False + + # Обработка объектов с методом dict + if hasattr(obj, "dict"): + try: + result = obj.dict() + serialized = True + return result + except Exception as _e: + serialized = False + + # Если ни один из методов не сработал, вызываем TypeError + if not serialized: + error_text = f"Object of type {type(obj).__name__} is not JSON serializable" + raise TypeError(error_text) def orjson_dumps(obj: Any, **kwargs: Any) -> bytes: """ - Сериализует объект в JSON с помощью orjson + Сериализация объекта с помощью orjson. Args: obj: Объект для сериализации - **kwargs: Дополнительные параметры для orjson.dumps + **kwargs: Дополнительные параметры Returns: - bytes: JSON в виде байтов + bytes: Сериализованный объект """ - # Используем правильную константу для orjson - option_flags = orjson.OPT_SERIALIZE_DATACLASS - if kwargs.get("indent"): - option_flags |= orjson.OPT_INDENT_2 - - return orjson.dumps(obj, default=default_json_encoder, option=option_flags) + return orjson.dumps(obj, default=default_json_encoder, **kwargs) def orjson_loads(data: Union[str, bytes]) -> Any: """ - Десериализует JSON с помощью orjson + Десериализация объекта с помощью orjson. Args: - data: JSON данные в виде строки или байтов + data: Строка или байты для десериализации Returns: Десериализованный объект @@ -68,51 +88,50 @@ def orjson_loads(data: Union[str, bytes]) -> Any: return orjson.loads(data) -class JSONEncoder: - """Кастомный JSON кодировщик на основе orjson""" - - @staticmethod - def encode(obj: Any) -> str: - """Encode object to JSON string""" - return orjson_dumps(obj).decode("utf-8") - - @staticmethod - def decode(data: Union[str, bytes]) -> Any: - """Decode JSON string to object""" - return orjson_loads(data) - - -# Создаем экземпляр для обратной совместимости -CustomJSONEncoder = JSONEncoder() - - -def fast_json_dumps(obj: Any, indent: bool = False) -> str: +class JSONEncoder(json.JSONEncoder): """ - Быстрая сериализация JSON + Расширенный JSON энкодер с поддержкой кастомной сериализации. + """ + + def default(self, obj: Any) -> Any: + """ + Метод для сериализации нестандартных типов. + + Args: + obj: Объект для сериализации + + Returns: + Сериализованное представление объекта + """ + try: + return default_json_encoder(obj) + except TypeError: + return super().default(obj) + + +def fast_json_dumps(obj: Any, **kwargs: Any) -> str: + """ + Быстрая сериализация объекта в JSON-строку. Args: obj: Объект для сериализации - indent: Форматировать с отступами + **kwargs: Дополнительные параметры Returns: - JSON строка + str: JSON-строка """ - return orjson_dumps(obj, indent=indent).decode("utf-8") + return json.dumps(obj, cls=JSONEncoder, **kwargs) -def fast_json_loads(data: Union[str, bytes]) -> Any: +def fast_json_loads(data: str, **kwargs: Any) -> Any: """ - Быстрая десериализация JSON + Быстрая десериализация JSON-строки. Args: - data: JSON данные + data: JSON-строка + **kwargs: Дополнительные параметры Returns: Десериализованный объект """ - return orjson_loads(data) - - -# Экспортируем для удобства -dumps = fast_json_dumps -loads = fast_json_loads + return json.loads(data, **kwargs) diff --git a/utils/extract_text.py b/utils/extract_text.py index b448598d..8163dcb2 100644 --- a/utils/extract_text.py +++ b/utils/extract_text.py @@ -2,66 +2,54 @@ Модуль для обработки HTML-фрагментов """ -import trafilatura - -from utils.logger import root_logger as logger +import re +from typing import Optional -def extract_text(html: str) -> str: +def extract_text(html_content: Optional[str]) -> str: """ - Извлекает чистый текст из HTML + Извлекает текст из HTML с помощью регулярных выражений. Args: - html: HTML строка + html_content (Optional[str]): HTML-строка для извлечения текста Returns: str: Извлеченный текст или пустая строка """ - try: - result = trafilatura.extract( - html, - include_comments=False, - include_tables=True, - include_formatting=False, - favor_precision=True, - ) - return result or "" - except Exception as e: - logger.error(f"Error extracting text: {e}") + if not html_content: return "" + # Удаляем HTML-теги + text = re.sub(r"<[^>]+>", " ", html_content) + + # Декодируем HTML-сущности + text = re.sub(r"&[a-zA-Z]+;", " ", text) + + # Заменяем несколько пробелов на один + text = re.sub(r"\s+", " ", text).strip() + + return text + def wrap_html_fragment(fragment: str) -> str: """ - Оборачивает HTML-фрагмент в полную HTML-структуру для корректной обработки. + Оборачивает HTML-фрагмент в полный HTML-документ. Args: - fragment: HTML-фрагмент для обработки + fragment (str): HTML-фрагмент Returns: str: Полный HTML-документ - - Example: - >>> wrap_html_fragment("Текст параграфа
") - 'Текст параграфа
' """ - if not fragment or not fragment.strip(): + if "" in fragment and "" in fragment: return fragment - # Проверяем, является ли контент полным HTML-документом - is_full_html = fragment.strip().startswith(" + return f""" -