matplobbot-shared 0.1.331__py3-none-any.whl → 0.1.332__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matplobbot-shared
3
- Version: 0.1.331
3
+ Version: 0.1.332
4
4
  Summary: Shared library for the Matplobbot ecosystem.
5
5
  Author: Ackrome
6
6
  Author-email: ivansergeyevich@gmail.com
@@ -1,30 +1,30 @@
1
- matplobbot_shared-0.1.331.dist-info/licenses/LICENSE,sha256=AmYEbPAbaZOpoiTYDrcnDjhoQqO_pe2QTX9CcvSB5z4,1072
1
+ matplobbot_shared-0.1.332.dist-info/licenses/LICENSE,sha256=AmYEbPAbaZOpoiTYDrcnDjhoQqO_pe2QTX9CcvSB5z4,1072
2
2
  shared_lib/__init__.py,sha256=XaPjbKhDIlcsuwUrjMkqt1OajCFldAG8z7dTNbVTh3g,46
3
3
  shared_lib/celery_app.py,sha256=Z2T-XWwFu_kkxj_mi15VC8ol9naMMUq7CpQHxVxhF9g,3390
4
4
  shared_lib/constants.py,sha256=iY9AgvSgE9-C0TY_GXbO8a3mb1Ada3FAhzH5CVcSsbk,541
5
- shared_lib/database.py,sha256=MpePUsjfiiWj5nlTmqjNDz485pZwQ9jKJlN1hGzcGq4,64081
5
+ shared_lib/database.py,sha256=1Da8qQ6b3RhxVQtcSTbnqf4j6JhtYcEXb-3_uUz2cEE,65231
6
6
  shared_lib/egress.py,sha256=C74dyj2LKMLGkanFd7tokjIJJKIJ8p70tA25TrBkwns,2979
7
- shared_lib/html_tools.py,sha256=bu8uavXm53deg86UHEHW8p93YhdJq3QAjgVEOt9dja8,4438
7
+ shared_lib/html_tools.py,sha256=ZzKMxAR2qNHdJGtGmu7i5Az_AwEkzCMoQFhmJxpowN0,4435
8
8
  shared_lib/i18n.py,sha256=HLXm8WjjcMtFiiD62DW8FgBlmwNZ6BoZ6MxuVO0wWCA,2199
9
- shared_lib/mail_bridge.py,sha256=KT7P2fJ6r88mA7jUmj_kpwQCYO1Lo6CisAN_WpDaaIs,10010
10
- shared_lib/models.py,sha256=QKD2H6m8-c8RKre6m2rVqaO8g2aydiVnqkWqq--ld4s,7681
11
- shared_lib/redis_client.py,sha256=LipBJYFbCOFOUsY9PXkFqBKXS4MzJewvu3wfw67YL1M,3234
9
+ shared_lib/mail_bridge.py,sha256=Cxk9k_KOWqgPnpMlM6Sime6MfWAlo7K5lMoekL7x0Vo,9248
10
+ shared_lib/models.py,sha256=FYcnolw3b728dyVpDa-t9FuGrOum0PONes9OmO7w354,8868
11
+ shared_lib/redis_client.py,sha256=cbF1tHOzEqlvBtmnEg7K0TSh2ZmORGuPYIbdqeE2TAs,3323
12
12
  shared_lib/request_context.py,sha256=A3SJT2EOPP5JVX7kDhu_RAJWUt5KN5WpMKWz13HQge0,1693
13
13
  shared_lib/schemas.py,sha256=Bo9Z6aebgZGQ1Bm8v9ggOviu_-e6wf7zBP2lCjIW6_U,21384
14
- shared_lib/tasks.py,sha256=frSmBvUoPbHkTlZsCB_8HNlGQarlN-mkY3xGOG_A-DI,28933
14
+ shared_lib/tasks.py,sha256=e0FMq93UEchDCBYg7qkqzMs8VmDFVE_IGhx9n5AN52w,29003
15
15
  shared_lib/telegram_bot_session.py,sha256=dw7p0d4x6tELYd8kaucaPz6cDECK-FCVyrdDAFYV4aM,6625
16
16
  shared_lib/telegram_http.py,sha256=iZA70ty7CdooXVO2_N1x6hf2MHoTlThxjlnRA4_OBtY,1597
17
17
  shared_lib/telegram_polling.py,sha256=6adHLV09IO3CrLT0enipdc9bc7WMRz5TUngiKWqs7cs,740
18
18
  shared_lib/telemetry.py,sha256=jW4DFXXHu4jR3qL47okrQ6n1SXkNLlPSFHZmXHWafKI,2908
19
- shared_lib/locales/en.json,sha256=DFfZZngJlTwK6JmRawP0R4-gi3pIGJmoUureeZDOcf4,29939
20
- shared_lib/locales/ru.json,sha256=QkE5aYRdIz5WpjUVghgxgQCCKLox0BJ6P25rinTIoaU,42582
19
+ shared_lib/locales/en.json,sha256=MmXknymct8Avg4Rm4saF4F9WEz5wWsjRXcQK_9pqlOo,29988
20
+ shared_lib/locales/ru.json,sha256=JKvdcCT2Z52yBkBpo4IlwIOhYmapniXULWrc1t-lyGM,42649
21
21
  shared_lib/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  shared_lib/services/broadcast_service.py,sha256=bXRoRNevnzWf27xAfVcyJma9Q7UO-T5Zg-6AyzRba64,9498
23
23
  shared_lib/services/calendar_sync_state.py,sha256=yR3OTufxIlz81Q3ZZHno6CNq-NPLlDN_Cr1jp8HM0lM,8494
24
24
  shared_lib/services/schedule_service.py,sha256=GOOl79s4xPo9sQttfd_ZYiz3tFaX03dYRKVfTCcJcQo,58691
25
25
  shared_lib/services/semantic_search.py,sha256=Q58gfcUUvxUrCw4SSntfw956YYFhv_6povhGciMIZ8U,3917
26
- shared_lib/services/university_api.py,sha256=WyqfESf1DN_Gr4so9ij6Oo3t0gAPu5JIdO9-SqN_GmQ,3603
27
- matplobbot_shared-0.1.331.dist-info/METADATA,sha256=WPbL1qCel0nOhdddHFtJJvubTwlBlLBAVc_elCYWNNc,851
28
- matplobbot_shared-0.1.331.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
29
- matplobbot_shared-0.1.331.dist-info/top_level.txt,sha256=L8mrC50YWCe19jmh_zrUZFvXSkhsnES5K6y027G1838,11
30
- matplobbot_shared-0.1.331.dist-info/RECORD,,
26
+ shared_lib/services/university_api.py,sha256=sD1PsBrEdZvKGsUkxK5brkfgSwc_0uBZckZBjFChNOE,3791
27
+ matplobbot_shared-0.1.332.dist-info/METADATA,sha256=XHiZI4AZkzV1Fw6ulOd3dwY8lneNdWR1bSPrGS9fwJU,851
28
+ matplobbot_shared-0.1.332.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
29
+ matplobbot_shared-0.1.332.dist-info/top_level.txt,sha256=L8mrC50YWCe19jmh_zrUZFvXSkhsnES5K6y027G1838,11
30
+ matplobbot_shared-0.1.332.dist-info/RECORD,,
shared_lib/database.py CHANGED
@@ -815,14 +815,17 @@ async def delete_old_inactive_subscriptions(days_inactive: int = 30):
815
815
 
816
816
 
817
817
  async def get_subscriptions_for_notification(notification_time: str) -> list:
818
+ try:
819
+ target_time = datetime.time.fromisoformat(notification_time)
820
+ except (TypeError, ValueError):
821
+ logger.warning("Ignoring invalid notification time: %r", notification_time)
822
+ return []
823
+
818
824
  async with get_session() as session:
819
- # notification_time comes as "HH:MM"
820
- # We use PostgreSQL TO_CHAR on the Time column
821
825
  stmt = select(UserScheduleSubscription).where(
822
826
  and_(
823
827
  UserScheduleSubscription.is_active.is_(True),
824
- func.to_char(UserScheduleSubscription.notification_time, "HH24:MI")
825
- == notification_time,
828
+ UserScheduleSubscription.notification_time == target_time,
826
829
  )
827
830
  )
828
831
  result = await session.execute(stmt)
@@ -1004,20 +1007,46 @@ async def batch_update_subscription_hashes(entity_type: str, entity_id: str, new
1004
1007
 
1005
1008
 
1006
1009
  # --- Cached Schedules ---
1010
+ def _cached_entity_name(entity_type: str, data: list | dict, fallback: str) -> str:
1011
+ """Extract a stable display name without querying/expanding JSON later."""
1012
+ if not isinstance(data, list):
1013
+ return fallback
1014
+ field_by_type = {
1015
+ "group": "group",
1016
+ "person": "lecturer_title",
1017
+ "auditorium": "auditorium",
1018
+ }
1019
+ field = field_by_type.get(entity_type)
1020
+ if not field:
1021
+ return fallback
1022
+ for lesson in data:
1023
+ if isinstance(lesson, dict):
1024
+ value = str(lesson.get(field) or "").strip()
1025
+ if value:
1026
+ return value[:255]
1027
+ return fallback
1028
+
1029
+
1007
1030
  async def upsert_cached_schedule(entity_type: str, entity_id: str, data: list | dict):
1008
1031
  json_data = json.loads(json.dumps(data, default=str)) # Ensure serializable
1032
+ entity_name = _cached_entity_name(entity_type, json_data, str(entity_id))
1009
1033
  async with get_session() as session:
1010
1034
  stmt = (
1011
1035
  pg_insert(CachedSchedule)
1012
1036
  .values(
1013
1037
  entity_type=entity_type,
1014
1038
  entity_id=str(entity_id),
1039
+ entity_name=entity_name,
1015
1040
  schedule_data=json_data,
1016
1041
  updated_at=datetime.datetime.now(),
1017
1042
  )
1018
1043
  .on_conflict_do_update(
1019
1044
  constraint="uq_cached_schedule_entity",
1020
- set_=dict(schedule_data=json_data, updated_at=datetime.datetime.now()),
1045
+ set_=dict(
1046
+ entity_name=entity_name,
1047
+ schedule_data=json_data,
1048
+ updated_at=datetime.datetime.now(),
1049
+ ),
1021
1050
  )
1022
1051
  )
1023
1052
  await session.execute(stmt)
@@ -1598,6 +1627,7 @@ async def merge_cached_schedule(
1598
1627
  # merged_data.sort(key=lambda x: (x.get('date', ''), x.get('beginLesson', '')))
1599
1628
 
1600
1629
  json_data = json.loads(json.dumps(merged_data, default=str))
1630
+ entity_name = _cached_entity_name(entity_type, json_data, str(entity_id))
1601
1631
 
1602
1632
  # 4. Upsert (вставка или обновление)
1603
1633
  stmt_insert = (
@@ -1605,12 +1635,17 @@ async def merge_cached_schedule(
1605
1635
  .values(
1606
1636
  entity_type=entity_type,
1607
1637
  entity_id=str(entity_id),
1638
+ entity_name=entity_name,
1608
1639
  schedule_data=json_data,
1609
1640
  updated_at=datetime.datetime.now(),
1610
1641
  )
1611
1642
  .on_conflict_do_update(
1612
1643
  constraint="uq_cached_schedule_entity",
1613
- set_=dict(schedule_data=json_data, updated_at=datetime.datetime.now()),
1644
+ set_=dict(
1645
+ entity_name=entity_name,
1646
+ schedule_data=json_data,
1647
+ updated_at=datetime.datetime.now(),
1648
+ ),
1614
1649
  )
1615
1650
  )
1616
1651
 
shared_lib/html_tools.py CHANGED
@@ -1,4 +1,4 @@
1
- import re
1
+ import re
2
2
 
3
3
  HTML_TAG_RE = re.compile(r"(</?([a-zA-Z0-9_-]+)(?:\s+[^>]*)?>)")
4
4
 
@@ -157,6 +157,7 @@
157
157
  "help_btn_lec_search": "📚 /lec_search - Search notes",
158
158
  "help_btn_lec_all": "📂 /lec_all - Browse notes",
159
159
  "help_btn_favorites": "⭐ /favorites - Favorites",
160
+ "help_btn_mail": "✉️ /mail - Mailboxes",
160
161
  "help_btn_settings": "⚙️ /settings - Settings",
161
162
  "help_btn_latex": "🧮 /latex - Render LaTeX",
162
163
  "help_btn_mermaid": "🎨 /mermaid - Render Mermaid",
@@ -136,6 +136,7 @@
136
136
  "help_btn_lec_search": "📚 /lec_search - Поиск по конспектам",
137
137
  "help_btn_lec_all": "📂 /lec_all - Просмотр конспектов",
138
138
  "help_btn_favorites": "⭐ /favorites - Избранное",
139
+ "help_btn_mail": "✉️ /mail - Почтовые ящики",
139
140
  "help_btn_settings": "⚙️ /settings - Настройки",
140
141
  "help_btn_latex": "🧮 /latex - Рендер LaTeX",
141
142
  "help_btn_mermaid": "🎨 /mermaid - Рендер Mermaid",
shared_lib/mail_bridge.py CHANGED
@@ -13,9 +13,10 @@ from html import escape
13
13
 
14
14
  from bs4 import BeautifulSoup
15
15
  from cryptography.fernet import Fernet
16
- from sqlalchemy import BigInteger, Boolean, Column, Integer, LargeBinary, String, UniqueConstraint
17
16
 
18
- from shared_lib.models import Base
17
+ from shared_lib.models import MailAccount
18
+
19
+ __all__ = ["MailAccount", "cipher", "seal", "unseal", "validate_host", "poll_mail", "parse_mail"]
19
20
 
20
21
  MAX_MAIL_BYTES = 35 * 1024 * 1024
21
22
  PASSWORD_PROMPT = "Пароль приложения для почты"
@@ -41,24 +42,6 @@ def sensitive_mail_update(event, data):
41
42
  )
42
43
 
43
44
 
44
- class MailAccount(Base):
45
- __tablename__ = "mail_accounts"
46
- __table_args__ = (
47
- UniqueConstraint("user_id", "address", "host", name="uq_mail_owner_address_host"),
48
- )
49
- id = Column(Integer, primary_key=True)
50
- user_id = Column(BigInteger, nullable=False, index=True)
51
- address = Column(String(320), nullable=False)
52
- host = Column(String(253), nullable=False)
53
- protocol = Column(String(8), nullable=False)
54
- port = Column(Integer, nullable=False, default=993)
55
- credential = Column(LargeBinary, nullable=False)
56
- enabled = Column(Boolean, nullable=False, default=True)
57
- checkpoint = Column(LargeBinary, nullable=False)
58
- pending = Column(LargeBinary, nullable=True)
59
- status = Column(String(80), nullable=False, default="ready")
60
-
61
-
62
45
  def cipher():
63
46
  return Fernet(os.environ["MAIL_CREDENTIAL_KEY"].encode("ascii"))
64
47
 
shared_lib/models.py CHANGED
@@ -36,7 +36,9 @@ class UserAction(Base):
36
36
  __tablename__ = "user_actions"
37
37
 
38
38
  id = Column(Integer, primary_key=True, autoincrement=True)
39
- user_id = Column(BigInteger, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True)
39
+ user_id = Column(
40
+ BigInteger, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True
41
+ )
40
42
  action_type = Column(String, nullable=False, index=True)
41
43
  action_details = Column(String, nullable=True)
42
44
  timestamp = Column(DateTime(timezone=True), server_default=func.now(), index=True)
@@ -78,7 +80,12 @@ class UserScheduleSubscription(Base):
78
80
  __tablename__ = "user_schedule_subscriptions"
79
81
 
80
82
  id = Column(Integer, primary_key=True, autoincrement=True)
81
- user_id = Column(BigInteger, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False)
83
+ user_id = Column(
84
+ BigInteger,
85
+ ForeignKey("users.user_id", ondelete="CASCADE"),
86
+ nullable=False,
87
+ index=True,
88
+ )
82
89
  chat_id = Column(BigInteger, nullable=False)
83
90
  entity_type = Column(String, nullable=False)
84
91
  entity_id = Column(String, nullable=False)
@@ -95,6 +102,11 @@ class UserScheduleSubscription(Base):
95
102
  UniqueConstraint(
96
103
  "chat_id", "entity_type", "entity_id", "notification_time", name="uq_schedule_subs"
97
104
  ),
105
+ Index(
106
+ "ix_user_subscriptions_notification_time_active",
107
+ "notification_time",
108
+ "is_active",
109
+ ),
98
110
  )
99
111
 
100
112
 
@@ -132,6 +144,7 @@ class CachedSchedule(Base):
132
144
  id = Column(Integer, primary_key=True, autoincrement=True)
133
145
  entity_type = Column(String, nullable=False)
134
146
  entity_id = Column(String, nullable=False)
147
+ entity_name = Column(String(255), nullable=True)
135
148
  schedule_data = Column(JSONB, nullable=False)
136
149
  updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
137
150
 
@@ -140,6 +153,31 @@ class CachedSchedule(Base):
140
153
  )
141
154
 
142
155
 
156
+ class MailAccount(Base):
157
+ """Encrypted mailbox connection owned by a Telegram user."""
158
+
159
+ __tablename__ = "mail_accounts"
160
+ __table_args__ = (
161
+ UniqueConstraint("user_id", "address", "host", name="uq_mail_owner_address_host"),
162
+ )
163
+ id = Column(Integer, primary_key=True)
164
+ user_id = Column(
165
+ BigInteger,
166
+ ForeignKey("users.user_id", ondelete="CASCADE"),
167
+ nullable=False,
168
+ index=True,
169
+ )
170
+ address = Column(String(320), nullable=False)
171
+ host = Column(String(253), nullable=False)
172
+ protocol = Column(String(8), nullable=False)
173
+ port = Column(Integer, nullable=False, default=993)
174
+ credential = Column(LargeBinary, nullable=False)
175
+ enabled = Column(Boolean, nullable=False, default=True)
176
+ checkpoint = Column(LargeBinary, nullable=False)
177
+ pending = Column(LargeBinary, nullable=True)
178
+ status = Column(String(80), nullable=False, default="ready")
179
+
180
+
143
181
  class SearchDocument(Base):
144
182
  __tablename__ = "search_documents"
145
183
 
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import os
3
4
 
4
5
  import redis.asyncio as redis
5
6
 
@@ -67,4 +68,7 @@ class RedisClient:
67
68
 
68
69
 
69
70
  # Создаем единственный экземпляр клиента
70
- redis_client = RedisClient(host="redis")
71
+ redis_client = RedisClient(
72
+ host=os.getenv("REDIS_HOST", "redis"),
73
+ port=int(os.getenv("REDIS_PORT", "6379")),
74
+ )
@@ -31,8 +31,10 @@ class RuzAPIClient:
31
31
  self.initial_delay = initial_delay
32
32
  self.backoff_factor = backoff_factor
33
33
 
34
- async def _request(self, sub_url: str) -> dict[str, Any] | list[dict[str, Any]]:
35
- full_url = self.HOST + sub_url
34
+ async def _request(
35
+ self, path: str, params: dict[str, str] | None = None
36
+ ) -> dict[str, Any] | list[dict[str, Any]]:
37
+ full_url = self.HOST + path
36
38
  ssl_context = ssl.create_default_context(cafile=certifi.where())
37
39
  last_exception = None
38
40
 
@@ -40,7 +42,10 @@ class RuzAPIClient:
40
42
  try:
41
43
  # Добавляем явный таймаут на запрос, чтобы не висеть бесконечно
42
44
  async with self.session.get(
43
- full_url, ssl=ssl_context, timeout=aiohttp.ClientTimeout(total=15)
45
+ full_url,
46
+ params=params,
47
+ ssl=ssl_context,
48
+ timeout=aiohttp.ClientTimeout(total=15),
44
49
  ) as response:
45
50
  if response.status == 200:
46
51
  try:
@@ -73,14 +78,17 @@ class RuzAPIClient:
73
78
 
74
79
  async def search(self, term: str, search_type: str) -> list[dict[str, Any]]:
75
80
  """Generic search function."""
76
- return await self._request(f"/api/search?term={term}&type={search_type}")
81
+ return await self._request(
82
+ "/api/search", params={"term": term, "type": search_type}
83
+ )
77
84
 
78
85
  async def get_schedule(
79
86
  self, entity_type: str, entity_id: str, start: str, finish: str
80
87
  ) -> list[dict[str, Any]]:
81
88
  """Generic function to get a schedule."""
82
89
  return await self._request(
83
- f"/api/schedule/{entity_type}/{entity_id}?start={start}&finish={finish}&lng=1"
90
+ f"/api/schedule/{entity_type}/{entity_id}",
91
+ params={"start": start, "finish": finish, "lng": "1"},
84
92
  )
85
93
 
86
94
 
shared_lib/tasks.py CHANGED
@@ -623,7 +623,9 @@ def compile_project_task(
623
623
  return {
624
624
  "status": "error",
625
625
  "message": f"Security restriction in {path}: LaTeX source must be UTF-8 text",
626
- "errors": [{"line": 1, "message": "LaTeX source must be UTF-8 text"}],
626
+ "errors": [
627
+ {"line": 1, "message": "LaTeX source must be UTF-8 text"}
628
+ ],
627
629
  }
628
630
  is_safe, reason = _validate_latex_source(file_text)
629
631
  if not is_safe: