matplobbot-shared 0.1.332__tar.gz → 0.1.333__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/PKG-INFO +1 -1
  2. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/matplobbot_shared.egg-info/PKG-INFO +1 -1
  3. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/setup.py +1 -1
  4. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/redis_client.py +39 -0
  5. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/services/university_api.py +1 -3
  6. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_keyboards.py +40 -0
  7. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_schedule_search_api.py +2 -6
  8. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_studio_router_api.py +1 -3
  9. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/LICENSE +0 -0
  10. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/README.md +0 -0
  11. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/matplobbot_shared.egg-info/SOURCES.txt +0 -0
  12. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/matplobbot_shared.egg-info/dependency_links.txt +0 -0
  13. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/matplobbot_shared.egg-info/requires.txt +0 -0
  14. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/matplobbot_shared.egg-info/top_level.txt +0 -0
  15. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/pyproject.toml +0 -0
  16. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/setup.cfg +0 -0
  17. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/__init__.py +0 -0
  18. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/celery_app.py +0 -0
  19. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/constants.py +0 -0
  20. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/database.py +0 -0
  21. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/egress.py +0 -0
  22. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/html_tools.py +0 -0
  23. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/i18n.py +0 -0
  24. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/locales/en.json +0 -0
  25. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/locales/ru.json +0 -0
  26. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/mail_bridge.py +0 -0
  27. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/models.py +0 -0
  28. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/request_context.py +0 -0
  29. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/schemas.py +0 -0
  30. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/services/__init__.py +0 -0
  31. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/services/broadcast_service.py +0 -0
  32. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/services/calendar_sync_state.py +0 -0
  33. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/services/schedule_service.py +0 -0
  34. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/services/semantic_search.py +0 -0
  35. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/tasks.py +0 -0
  36. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/telegram_bot_session.py +0 -0
  37. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/telegram_http.py +0 -0
  38. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/telegram_polling.py +0 -0
  39. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/shared_lib/telemetry.py +0 -0
  40. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_auth_flow.py +0 -0
  41. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_authorization_guards.py +0 -0
  42. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_broadcast_service.py +0 -0
  43. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_build_audit_requirements.py +0 -0
  44. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_calendar_api.py +0 -0
  45. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_correlation_middleware.py +0 -0
  46. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_dashboard_text_encoding.py +0 -0
  47. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_egress.py +0 -0
  48. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_fastapi_config.py +0 -0
  49. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_latex_task_security.py +0 -0
  50. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_localization_completeness.py +0 -0
  51. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_mail_bridge.py +0 -0
  52. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_myschedule_filters_settings.py +0 -0
  53. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_openapi_docs.py +0 -0
  54. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_proxy_cleaner.py +0 -0
  55. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_proxy_config.py +0 -0
  56. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_rate_limit.py +0 -0
  57. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_schedule_service_calendar.py +0 -0
  58. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_scheduler_html_splitter.py +0 -0
  59. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_scheduler_service.py +0 -0
  60. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_search_center_helpers.py +0 -0
  61. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_stats_action_users_api.py +0 -0
  62. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_stats_avatar_security.py +0 -0
  63. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_stats_export_api.py +0 -0
  64. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_stats_leaderboard_api.py +0 -0
  65. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_stats_profile_api.py +0 -0
  66. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_stats_proxy_diagnostics_api.py +0 -0
  67. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_stats_send_message_api.py +0 -0
  68. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_telegram_bot_session.py +0 -0
  69. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_telegram_polling.py +0 -0
  70. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_telemetry.py +0 -0
  71. {matplobbot_shared-0.1.332 → matplobbot_shared-0.1.333}/tests/test_version_bumper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matplobbot-shared
3
- Version: 0.1.332
3
+ Version: 0.1.333
4
4
  Summary: Shared library for the Matplobbot ecosystem.
5
5
  Author: Ackrome
6
6
  Author-email: ivansergeyevich@gmail.com
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matplobbot-shared
3
- Version: 0.1.332
3
+ Version: 0.1.333
4
4
  Summary: Shared library for the Matplobbot ecosystem.
5
5
  Author: Ackrome
6
6
  Author-email: ivansergeyevich@gmail.com
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="matplobbot-shared",
5
- version="0.1.332", # Bump version
5
+ version="0.1.333", # Bump version
6
6
  packages=find_packages(include=["shared_lib", "shared_lib.*"]),
7
7
  description="Shared library for the Matplobbot ecosystem.",
8
8
  author="Ackrome",
@@ -8,6 +8,11 @@ logger = logging.getLogger(__name__)
8
8
 
9
9
  # TTL для кэша в секундах (например, 1 час)
10
10
  CACHE_TTL = 3600
11
+ CALLBACK_PATH_KEY_PREFIX = "callback_path:"
12
+
13
+
14
+ def _is_callback_path_hash(value: str) -> bool:
15
+ return len(value) == 16 and all(char in "0123456789abcdef" for char in value)
11
16
 
12
17
 
13
18
  class RedisClient:
@@ -66,6 +71,40 @@ class RedisClient:
66
71
  except Exception as e:
67
72
  logger.error(f"Ошибка при очистке кэша Redis: {e}")
68
73
 
74
+ async def set_callback_path(self, path_hash: str, path: str, ttl: int):
75
+ """Persist a Telegram callback hash mapping without serializing secrets."""
76
+ if not _is_callback_path_hash(path_hash):
77
+ raise ValueError("invalid callback path hash")
78
+ if ttl <= 0:
79
+ raise ValueError("callback path TTL must be positive")
80
+ try:
81
+ await self.client.set(
82
+ f"{CALLBACK_PATH_KEY_PREFIX}{path_hash}",
83
+ path,
84
+ ex=ttl,
85
+ )
86
+ except Exception as e:
87
+ logger.error("Ошибка при записи callback path в Redis для hash=%s: %s", path_hash, e)
88
+
89
+ async def get_callback_path(self, path_hash: str) -> str | None:
90
+ """Load a callback mapping created by this application."""
91
+ if not _is_callback_path_hash(path_hash):
92
+ return None
93
+ try:
94
+ return await self.client.get(f"{CALLBACK_PATH_KEY_PREFIX}{path_hash}")
95
+ except Exception as e:
96
+ logger.error("Ошибка при чтении callback path из Redis для hash=%s: %s", path_hash, e)
97
+ return None
98
+
99
+ async def clear_callback_paths(self):
100
+ """Remove persistent callback mappings during an explicit admin cache clear."""
101
+ try:
102
+ async for key in self.client.scan_iter(f"{CALLBACK_PATH_KEY_PREFIX}*"):
103
+ await self.client.delete(key)
104
+ logger.info("Все callback path mappings в Redis очищены.")
105
+ except Exception as e:
106
+ logger.error("Ошибка при очистке callback path mappings в Redis: %s", e)
107
+
69
108
 
70
109
  # Создаем единственный экземпляр клиента
71
110
  redis_client = RedisClient(
@@ -78,9 +78,7 @@ class RuzAPIClient:
78
78
 
79
79
  async def search(self, term: str, search_type: str) -> list[dict[str, Any]]:
80
80
  """Generic search function."""
81
- return await self._request(
82
- "/api/search", params={"term": term, "type": search_type}
83
- )
81
+ return await self._request("/api/search", params={"term": term, "type": search_type})
84
82
 
85
83
  async def get_schedule(
86
84
  self, entity_type: str, entity_id: str, start: str, finish: str
@@ -1,7 +1,9 @@
1
+ import asyncio
1
2
  import importlib
2
3
  import sys
3
4
  import types
4
5
  import unittest
6
+ from unittest.mock import AsyncMock, patch
5
7
 
6
8
 
7
9
  def _install_matplobblib_stub() -> None:
@@ -90,3 +92,41 @@ class TestTelegramWebAppKeyboards(unittest.IsolatedAsyncioTestCase):
90
92
  buttons = [button for row in help_markup.inline_keyboard for button in row]
91
93
  self.assertFalse(any(button.web_app for button in buttons))
92
94
  self.assertIn("help_btn_matp_all", [button.text for button in buttons])
95
+
96
+
97
+ @unittest.skipUnless(KEYBOARDS_AVAILABLE, "bot keyboard dependencies are not installed")
98
+ class TestCallbackPathPersistence(unittest.IsolatedAsyncioTestCase):
99
+ async def asyncSetUp(self):
100
+ kb.code_path_cache.clear()
101
+
102
+ async def asyncTearDown(self):
103
+ kb.code_path_cache.clear()
104
+
105
+ async def test_resolve_code_path_restores_mapping_from_redis(self):
106
+ path_hash = "a" * 16
107
+ with patch.object(
108
+ kb.redis_client,
109
+ "get_callback_path",
110
+ new=AsyncMock(return_value="owner/repo/README.md"),
111
+ ) as get_callback_path:
112
+ value = await kb.resolve_code_path(path_hash)
113
+
114
+ self.assertEqual(value, "owner/repo/README.md")
115
+ self.assertEqual(kb.code_path_cache.get(path_hash), "owner/repo/README.md")
116
+ get_callback_path.assert_awaited_once_with(path_hash)
117
+
118
+ async def test_cache_write_persists_mapping_to_redis(self):
119
+ path_hash = "b" * 16
120
+ with patch.object(
121
+ kb.redis_client,
122
+ "set_callback_path",
123
+ new=AsyncMock(),
124
+ ) as set_callback_path:
125
+ kb.code_path_cache[path_hash] = "owner/repo/src/main.py"
126
+ await asyncio.sleep(0)
127
+
128
+ set_callback_path.assert_awaited_once_with(
129
+ path_hash,
130
+ "owner/repo/src/main.py",
131
+ ttl=kb.CALLBACK_PATH_TTL_SECONDS,
132
+ )
@@ -566,9 +566,7 @@ class TestRuzAPIClient(unittest.IsolatedAsyncioTestCase):
566
566
  await client.get_schedule("group", "M80/101", "2026-04-01", "2026-04-30")
567
567
 
568
568
  self.assertEqual(session.calls[0][0], "https://ruz.fa.ru/api/search")
569
- self.assertEqual(
570
- session.calls[0][1]["params"], {"term": "М80 & 101", "type": "group"}
571
- )
569
+ self.assertEqual(session.calls[0][1]["params"], {"term": "М80 & 101", "type": "group"})
572
570
  self.assertEqual(session.calls[1][0], "https://ruz.fa.ru/api/schedule/group/M80/101")
573
571
  self.assertEqual(
574
572
  session.calls[1][1]["params"],
@@ -578,9 +576,7 @@ class TestRuzAPIClient(unittest.IsolatedAsyncioTestCase):
578
576
 
579
577
  class TestCachedScheduleLabels(unittest.TestCase):
580
578
  def test_cached_entity_name_uses_type_specific_display_field(self):
581
- self.assertEqual(
582
- _cached_entity_name("group", [{"group": "M80-101"}], "group-1"), "M80-101"
583
- )
579
+ self.assertEqual(_cached_entity_name("group", [{"group": "M80-101"}], "group-1"), "M80-101")
584
580
  self.assertEqual(
585
581
  _cached_entity_name("person", [{"lecturer_title": "Ivan Petrov"}], "person-1"),
586
582
  "Ivan Petrov",
@@ -82,9 +82,7 @@ class TestStudioRouterAPI(unittest.TestCase):
82
82
  self.app.dependency_overrides.clear()
83
83
 
84
84
  def test_project_filename_rejects_paths_and_control_characters(self):
85
- self.assertEqual(
86
- studio_router._sanitize_project_filename("diagram.png"), "diagram.png"
87
- )
85
+ self.assertEqual(studio_router._sanitize_project_filename("diagram.png"), "diagram.png")
88
86
  for filename in ("../secret.txt", "nested/image.png", r"nested\image.png", "bad\x00.txt"):
89
87
  with self.subTest(filename=filename), self.assertRaises(HTTPException) as raised:
90
88
  studio_router._sanitize_project_filename(filename)