article-backup 0.3.7__tar.gz → 0.3.8__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 (29) hide show
  1. {article_backup-0.3.7 → article_backup-0.3.8}/PKG-INFO +1 -1
  2. {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/PKG-INFO +1 -1
  3. {article_backup-0.3.7 → article_backup-0.3.8}/pyproject.toml +1 -1
  4. {article_backup-0.3.7 → article_backup-0.3.8}/src/downloader.py +14 -1
  5. {article_backup-0.3.7 → article_backup-0.3.8}/src/sponsr.py +1 -0
  6. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_incremental_sync.py +53 -12
  7. {article_backup-0.3.7 → article_backup-0.3.8}/LICENSE +0 -0
  8. {article_backup-0.3.7 → article_backup-0.3.8}/README.md +0 -0
  9. {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/SOURCES.txt +0 -0
  10. {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/dependency_links.txt +0 -0
  11. {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/entry_points.txt +0 -0
  12. {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/requires.txt +0 -0
  13. {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/top_level.txt +0 -0
  14. {article_backup-0.3.7 → article_backup-0.3.8}/backup.py +0 -0
  15. {article_backup-0.3.7 → article_backup-0.3.8}/setup.cfg +0 -0
  16. {article_backup-0.3.7 → article_backup-0.3.8}/src/__init__.py +0 -0
  17. {article_backup-0.3.7 → article_backup-0.3.8}/src/boosty.py +0 -0
  18. {article_backup-0.3.7 → article_backup-0.3.8}/src/config.py +0 -0
  19. {article_backup-0.3.7 → article_backup-0.3.8}/src/database.py +0 -0
  20. {article_backup-0.3.7 → article_backup-0.3.8}/src/utils.py +0 -0
  21. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_asset_dedup.py +0 -0
  22. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_boosty_empty_link.py +0 -0
  23. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_boosty_normalize.py +0 -0
  24. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_config_hardening.py +0 -0
  25. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_slug_safety.py +0 -0
  26. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_sponsr_formatting_fix.py +0 -0
  27. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_sponsr_normalize.py +0 -0
  28. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_sponsr_tags.py +0 -0
  29. {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_video_embed.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: article-backup
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: Локальный бэкап статей с Sponsr.ru и Boosty.to в Markdown с Hugo-интеграцией
5
5
  Author-email: Eugene Chaykin <eugene@chayk.in>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: article-backup
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: Локальный бэкап статей с Sponsr.ru и Boosty.to в Markdown с Hugo-интеграцией
5
5
  Author-email: Eugene Chaykin <eugene@chayk.in>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "article-backup"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  description = "Локальный бэкап статей с Sponsr.ru и Boosty.to в Markdown с Hugo-интеграцией"
5
5
  readme = "README.md"
6
6
  license = {text = "Apache-2.0"}
@@ -84,6 +84,7 @@ class BaseDownloader(ABC):
84
84
  PLATFORM: str = ""
85
85
  MAX_WORKERS: int = 5
86
86
  TIMEOUT: tuple = (5, 60)
87
+ FETCH_FULL_POST_IN_SYNC: bool = False
87
88
 
88
89
  def __init__(self, config: Config, source: Source, db: Database):
89
90
  self.config = config
@@ -146,7 +147,19 @@ class BaseDownloader(ABC):
146
147
  print(f" Найдено постов: {len(posts)}, новых: {len(new_posts)}")
147
148
 
148
149
  for raw_post in new_posts:
149
- post = self._parse_post(raw_post)
150
+ post_id = str(raw_post.get('id', raw_post.get('post_id')))
151
+
152
+ # Для некоторых платформ/эндпоинтов список постов может содержать
153
+ # урезанный или искажённый контент. В этом случае догружаем полный
154
+ # пост по ID (как в single mode), чтобы сохранить консистентный
155
+ # результат конвертации Markdown.
156
+ post = None
157
+ if self.FETCH_FULL_POST_IN_SYNC and post_id:
158
+ post = self.fetch_post(post_id)
159
+
160
+ if not post:
161
+ post = self._parse_post(raw_post)
162
+
150
163
  if post:
151
164
  self._save_post(post)
152
165
 
@@ -29,6 +29,7 @@ class SponsorDownloader(BaseDownloader):
29
29
  """Загрузчик статей с Sponsr.ru"""
30
30
 
31
31
  PLATFORM = "sponsr"
32
+ FETCH_FULL_POST_IN_SYNC = True
32
33
 
33
34
  def __init__(self, config: Config, source: Source, db: Database):
34
35
  self._project_id: str | None = None
@@ -6,10 +6,54 @@ from unittest.mock import MagicMock, patch
6
6
  from src.config import Auth, Config, Source
7
7
  from src.database import Database
8
8
  from src.boosty import BoostyDownloader
9
+ from src.downloader import Post
9
10
  from src.sponsr import SponsorDownloader
10
11
 
11
12
 
12
13
  class IncrementalSyncTests(unittest.TestCase):
14
+ def test_sponsr_sync_fetches_full_post_for_new_items(self):
15
+ """Sponsr sync догружает полный пост, а не берёт урезанный HTML из списка."""
16
+ with tempfile.TemporaryDirectory() as tmp:
17
+ tmp_path = Path(tmp)
18
+ config = Config(output_dir=tmp_path, auth=Auth())
19
+ source = Source(platform='sponsr', author='test_author')
20
+
21
+ with Database(tmp_path / 'test.db') as db:
22
+ with patch('src.sponsr.load_cookie', return_value='fake_cookie'):
23
+ dl = SponsorDownloader(config, source, db)
24
+
25
+ raw_posts = [{
26
+ 'post_id': '137432',
27
+ 'post_title': 'Test title',
28
+ 'post_date': '2025-01-01T00:00:00',
29
+ 'post_url': '/test_author/137432/test-title',
30
+ 'post_text': {'text': '<p>bad<strong>html</strong></p>'},
31
+ }]
32
+
33
+ full_post = Post(
34
+ post_id='137432',
35
+ title='Test title',
36
+ content_html='<p>good <strong>html</strong></p>',
37
+ post_date='2025-01-01T00:00:00',
38
+ source_url='https://sponsr.ru/test_author/137432/test-title',
39
+ tags=[],
40
+ assets=[],
41
+ )
42
+
43
+ with patch.object(dl, 'fetch_posts_list', return_value=raw_posts):
44
+ with patch.object(dl, 'fetch_post', return_value=full_post) as mocked_fetch_post:
45
+ dl.sync()
46
+
47
+ mocked_fetch_post.assert_called_once_with('137432')
48
+
49
+ saved = db.get_post('sponsr', 'test_author', '137432')
50
+ if saved is None:
51
+ self.fail('Пост не был сохранён в БД')
52
+ md_path = Path(saved.local_path) / 'index.md'
53
+ content = md_path.read_text(encoding='utf-8')
54
+ self.assertIn('good **html**', content)
55
+ self.assertNotIn('bad **html**', content)
56
+
13
57
  def test_sponsr_incremental_stops_on_clean_chunk(self):
14
58
  """Sponsr: останавливается на чистом чанке."""
15
59
  with tempfile.TemporaryDirectory() as tmp:
@@ -48,12 +92,11 @@ class IncrementalSyncTests(unittest.TestCase):
48
92
  call_count += 1
49
93
  return resp
50
94
 
51
- dl.session.get = mock_get
52
-
53
95
  # Все посты уже существуют
54
96
  existing_ids = {str(i) for i in range(1, 41)}
55
-
56
- posts = dl.fetch_posts_list(existing_ids, incremental=True, safety_chunks=1)
97
+
98
+ with patch.object(dl.session, 'get', side_effect=mock_get):
99
+ posts = dl.fetch_posts_list(existing_ids, incremental=True, safety_chunks=1)
57
100
 
58
101
  # Должен остановиться после первого чанка + 1 защитный
59
102
  self.assertEqual(call_count, 2)
@@ -102,12 +145,11 @@ class IncrementalSyncTests(unittest.TestCase):
102
145
  call_count += 1
103
146
  return resp
104
147
 
105
- dl.session.get = mock_get
106
-
107
148
  # Первые 5 постов новые, остальные существуют
108
149
  existing_ids = {str(i) for i in range(6, 41)}
109
-
110
- posts = dl.fetch_posts_list(existing_ids, incremental=True, safety_chunks=1)
150
+
151
+ with patch.object(dl.session, 'get', side_effect=mock_get):
152
+ posts = dl.fetch_posts_list(existing_ids, incremental=True, safety_chunks=1)
111
153
 
112
154
  # Должен загрузить: первый (с новыми) + второй (чистый) + остановиться
113
155
  # Всего 2 чанка с данными (call_count может быть 2 или 3 в зависимости от реализации)
@@ -146,11 +188,10 @@ class IncrementalSyncTests(unittest.TestCase):
146
188
  call_count += 1
147
189
  return resp
148
190
 
149
- dl.session.get = mock_get
150
-
151
191
  existing_ids = {f"uuid-{i}" for i in range(1, 41)}
152
-
153
- posts = dl.fetch_posts_list(existing_ids, incremental=True, safety_chunks=1)
192
+
193
+ with patch.object(dl.session, 'get', side_effect=mock_get):
194
+ posts = dl.fetch_posts_list(existing_ids, incremental=True, safety_chunks=1)
154
195
 
155
196
  self.assertEqual(call_count, 2)
156
197
  self.assertEqual(len(posts), 40)
File without changes
File without changes
File without changes
File without changes