matplobbot-shared 0.1.31__tar.gz → 0.1.33__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.

Potentially problematic release.


This version of matplobbot-shared might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matplobbot-shared
3
- Version: 0.1.31
3
+ Version: 0.1.33
4
4
  Summary: Shared library for the Matplobbot ecosystem (database, services, i18n).
5
5
  Author: Ackrome
6
6
  Author-email: ivansergeyevich@gmail.com
@@ -8,6 +8,7 @@ Requires-Python: >=3.11
8
8
  Requires-Dist: asyncpg
9
9
  Requires-Dist: aiohttp
10
10
  Requires-Dist: certifi
11
+ Requires-Dist: redis
11
12
  Dynamic: author
12
13
  Dynamic: author-email
13
14
  Dynamic: requires-dist
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matplobbot-shared
3
- Version: 0.1.31
3
+ Version: 0.1.33
4
4
  Summary: Shared library for the Matplobbot ecosystem (database, services, i18n).
5
5
  Author: Ackrome
6
6
  Author-email: ivansergeyevich@gmail.com
@@ -8,6 +8,7 @@ Requires-Python: >=3.11
8
8
  Requires-Dist: asyncpg
9
9
  Requires-Dist: aiohttp
10
10
  Requires-Dist: certifi
11
+ Requires-Dist: redis
11
12
  Dynamic: author
12
13
  Dynamic: author-email
13
14
  Dynamic: requires-dist
@@ -8,6 +8,7 @@ matplobbot_shared.egg-info/top_level.txt
8
8
  shared_lib/__init__.py
9
9
  shared_lib/database.py
10
10
  shared_lib/i18n.py
11
+ shared_lib/redis_client.py
11
12
  shared_lib/services/__init__.py
12
13
  shared_lib/services/schedule_service.py
13
14
  shared_lib/services/university_api.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="matplobbot-shared",
5
- version="0.1.31", # Let's use the version from your requirements.txt
5
+ version="0.1.33", # Let's use the version from your requirements.txt
6
6
  packages=find_packages(include=['shared_lib', 'shared_lib.*']),
7
7
  description="Shared library for the Matplobbot ecosystem (database, services, i18n).",
8
8
  author="Ackrome",
@@ -11,7 +11,8 @@ setup(
11
11
  install_requires=[
12
12
  "asyncpg",
13
13
  "aiohttp", # Specify versions as needed
14
- "certifi"
14
+ "certifi",
15
+ "redis"
15
16
  ],
16
17
  # This tells setuptools that the package data (like .json files) should be included
17
18
  package_data={
@@ -108,6 +108,7 @@ async def init_db():
108
108
  entity_id TEXT NOT NULL,
109
109
  entity_name TEXT NOT NULL,
110
110
  notification_time TIME NOT NULL,
111
+ last_schedule_hash TEXT,
111
112
  is_active BOOLEAN DEFAULT TRUE,
112
113
  UNIQUE(user_id, entity_type, entity_id)
113
114
  )
@@ -254,13 +255,32 @@ async def remove_schedule_subscription(subscription_id: int, user_id: int) -> st
254
255
 
255
256
  async def get_subscriptions_for_notification(notification_time: str) -> list:
256
257
  async with pool.acquire() as connection:
258
+ # Modified to select the subscription ID and the last hash
257
259
  rows = await connection.fetch("""
258
- SELECT user_id, entity_type, entity_id, entity_name
260
+ SELECT id, user_id, entity_type, entity_id, entity_name, last_schedule_hash
259
261
  FROM user_schedule_subscriptions
260
262
  WHERE is_active = TRUE AND TO_CHAR(notification_time, 'HH24:MI') = $1
261
263
  """, notification_time)
262
264
  return [dict(row) for row in rows]
263
265
 
266
+ async def get_all_active_subscriptions() -> list:
267
+ """Fetches all active schedule subscriptions from the database."""
268
+ if not pool:
269
+ raise ConnectionError("Database pool is not initialized.")
270
+ async with pool.acquire() as connection:
271
+ rows = await connection.fetch("""
272
+ SELECT id, user_id, entity_type, entity_id, entity_name, last_schedule_hash
273
+ FROM user_schedule_subscriptions WHERE is_active = TRUE
274
+ """)
275
+ return [dict(row) for row in rows]
276
+
277
+ async def update_subscription_hash(subscription_id: int, new_hash: str):
278
+ """Updates the schedule hash for a specific subscription."""
279
+ if not pool:
280
+ raise ConnectionError("Database pool is not initialized.")
281
+ async with pool.acquire() as connection:
282
+ await connection.execute("UPDATE user_schedule_subscriptions SET last_schedule_hash = $1 WHERE id = $2", new_hash, subscription_id)
283
+
264
284
  # --- FastAPI Specific Queries ---
265
285
  async def get_leaderboard_data_from_db(db_conn):
266
286
  # The timestamp is stored in UTC (TIMESTAMPTZ). We convert it to Moscow time for display.
@@ -0,0 +1,46 @@
1
+ import redis.asyncio as redis
2
+ import json
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ # TTL для кэша в секундах (например, 1 час)
8
+ CACHE_TTL = 3600
9
+
10
+ class RedisClient:
11
+ def __init__(self, host='localhost', port=6379):
12
+ # Используем connection_pool для более эффективного управления соединениями
13
+ self.pool = redis.ConnectionPool(host=host, port=port, db=0, decode_responses=True)
14
+ self.client = redis.Redis(connection_pool=self.pool)
15
+
16
+ async def set_user_cache(self, user_id: int, key: str, data: dict, ttl: int = CACHE_TTL):
17
+ """Сохраняет данные в кэш для конкретного пользователя."""
18
+ try:
19
+ redis_key = f"user_cache:{user_id}:{key}"
20
+ await self.client.set(redis_key, json.dumps(data), ex=ttl)
21
+ except Exception as e:
22
+ logger.error(f"Ошибка при записи в Redis для user_id={user_id}, key={key}: {e}")
23
+
24
+ async def get_user_cache(self, user_id: int, key: str) -> dict | None:
25
+ """Получает данные из кэша для конкретного пользователя."""
26
+ try:
27
+ redis_key = f"user_cache:{user_id}:{key}"
28
+ data = await self.client.get(redis_key)
29
+ if data:
30
+ return json.loads(data)
31
+ return None
32
+ except Exception as e:
33
+ logger.error(f"Ошибка при чтении из Redis для user_id={user_id}, key={key}: {e}")
34
+ return None
35
+
36
+ async def clear_all_user_cache(self):
37
+ """Очищает весь пользовательский кэш (ключи, начинающиеся с 'user_cache:')."""
38
+ try:
39
+ async for key in self.client.scan_iter("user_cache:*"):
40
+ await self.client.delete(key)
41
+ logger.info("Весь пользовательский кэш в Redis очищен.")
42
+ except Exception as e:
43
+ logger.error(f"Ошибка при очистке кэша Redis: {e}")
44
+
45
+ # Создаем единственный экземпляр клиента
46
+ redis_client = RedisClient(host='redis')
@@ -23,8 +23,8 @@ def format_schedule(schedule_data: List[Dict[str, Any]], lang: str, entity_name:
23
23
  """Formats a list of lessons into a readable daily schedule."""
24
24
  if not schedule_data:
25
25
  # Different message for single day vs week
26
- no_lessons_key = "schedule_no_lessons_week" if is_week_view else "schedule_no_lessons_day"
27
- return f"🗓 *Расписание для \"{entity_name}\"*\n\n{translator.gettext(lang, no_lessons_key)}"
26
+ no_lessons_key = "schedule_no_lessons_week" if is_week_view else "schedule_no_lessons_day" # This was Russian text
27
+ return translator.gettext(lang, "schedule_header_for", entity_name=entity_name) + f"\n\n{translator.gettext(lang, no_lessons_key)}"
28
28
 
29
29
  # Group lessons by date
30
30
  days = defaultdict(list)
@@ -41,24 +41,24 @@ def format_schedule(schedule_data: List[Dict[str, Any]], lang: str, entity_name:
41
41
  formatted_lessons = []
42
42
  for lesson in sorted(lessons, key=lambda x: x['beginLesson']):
43
43
  lesson_details = [
44
- f"`{lesson['beginLesson']} - {lesson['endLesson']}`",
44
+ f"`{lesson['beginLesson']} - {lesson['endLesson']} | {lesson['auditorium']}`",
45
45
  f"{lesson['discipline']} | {names_shorter[lesson['kindOfWork']]}"
46
46
  ]
47
47
 
48
48
  if entity_type == 'group':
49
- lesson_details.append(f"*{lesson['auditorium']}* | {lesson['lecturer_title'].replace('_',' ')}")
49
+ lesson_details.append(f"{lesson['lecturer_title'].replace('_',' ')}\n{lesson.get('lecturerEmail', 'Почта не указана')}")
50
50
  elif entity_type == 'person': # Lecturer
51
- lesson_details.append(f"*{lesson['auditorium']}* | {lesson.get('group', 'Группа не указана')}")
51
+ lesson_details.append(f" {lesson.get('group', 'Группа не указана')}")
52
52
  elif entity_type == 'auditorium':
53
- lesson_details.append(f"{lesson.get('group', 'Группа не указана')} | {lesson['lecturer_title'].replace('_',' ')}")
53
+ lesson_details.append(f"{lesson.get('group', 'Группа не указана')} | {lesson['lecturer_title'].replace('_',' ')}\n{lesson.get('lecturerEmail', 'Почта не указана')}")
54
54
  else: # Fallback to a generic format
55
- lesson_details.append(f"*{lesson['auditorium']}* | {lesson['lecturer_title'].replace('_',' ')}")
55
+ lesson_details.append(f"{lesson['lecturer_title'].replace('_',' ')}")
56
56
 
57
57
  formatted_lessons.append("\n".join(lesson_details))
58
58
 
59
59
  formatted_days.append(f"{day_header}\n" + "\n\n".join(formatted_lessons))
60
60
 
61
- main_header = f"🗓 *Расписание для \"{entity_name}\"*"
61
+ main_header = translator.gettext(lang, "schedule_header_for", entity_name=entity_name)
62
62
  return f"{main_header}\n\n" + "\n\n---\n\n".join(formatted_days)
63
63
 
64
64
  def generate_ical_from_schedule(schedule_data: List[Dict[str, Any]], entity_name: str) -> str: