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.
- {article_backup-0.3.7 → article_backup-0.3.8}/PKG-INFO +1 -1
- {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/PKG-INFO +1 -1
- {article_backup-0.3.7 → article_backup-0.3.8}/pyproject.toml +1 -1
- {article_backup-0.3.7 → article_backup-0.3.8}/src/downloader.py +14 -1
- {article_backup-0.3.7 → article_backup-0.3.8}/src/sponsr.py +1 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_incremental_sync.py +53 -12
- {article_backup-0.3.7 → article_backup-0.3.8}/LICENSE +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/README.md +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/SOURCES.txt +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/dependency_links.txt +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/entry_points.txt +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/requires.txt +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/article_backup.egg-info/top_level.txt +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/backup.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/setup.cfg +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/src/__init__.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/src/boosty.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/src/config.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/src/database.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/src/utils.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_asset_dedup.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_boosty_empty_link.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_boosty_normalize.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_config_hardening.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_slug_safety.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_sponsr_formatting_fix.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_sponsr_normalize.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_sponsr_tags.py +0 -0
- {article_backup-0.3.7 → article_backup-0.3.8}/tests/test_video_embed.py +0 -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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|