oldnews 0.8.0__py3-none-any.whl → 0.9.0__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.
- oldnews/__main__.py +1 -2
- oldnews/data/__init__.py +6 -5
- oldnews/data/config.py +6 -3
- oldnews/data/last_grab.py +11 -19
- oldnews/data/local_articles.py +107 -246
- oldnews/data/local_data.py +45 -0
- oldnews/data/local_folders.py +12 -21
- oldnews/data/local_subscriptions.py +26 -67
- oldnews/data/local_unread.py +5 -3
- oldnews/data/models/__init__.py +24 -0
- oldnews/data/models/local_article.py +81 -0
- oldnews/data/models/local_folder.py +19 -0
- oldnews/data/models/local_state.py +25 -0
- oldnews/data/models/local_subscription.py +41 -0
- oldnews/data/navigation_state.py +11 -20
- oldnews/data/reset.py +7 -1
- oldnews/oldnews.py +8 -1
- oldnews/screens/information_display.py +1 -1
- oldnews/screens/main.py +55 -47
- oldnews/sync.py +40 -35
- oldnews/widgets/_next_matching_option.py +1 -1
- oldnews/widgets/article_list.py +10 -9
- oldnews/widgets/navigation.py +24 -14
- {oldnews-0.8.0.dist-info → oldnews-0.9.0.dist-info}/METADATA +2 -2
- oldnews-0.9.0.dist-info/RECORD +44 -0
- {oldnews-0.8.0.dist-info → oldnews-0.9.0.dist-info}/WHEEL +1 -1
- oldnews/data/db.py +0 -97
- oldnews/data/tools.py +0 -45
- oldnews-0.8.0.dist-info/RECORD +0 -40
- {oldnews-0.8.0.dist-info → oldnews-0.9.0.dist-info}/entry_points.txt +0 -0
oldnews/__main__.py
CHANGED
|
@@ -9,7 +9,7 @@ from operator import attrgetter
|
|
|
9
9
|
##############################################################################
|
|
10
10
|
# Local imports.
|
|
11
11
|
from . import __doc__, __version__
|
|
12
|
-
from .data import
|
|
12
|
+
from .data import reset_data
|
|
13
13
|
from .data.locations import config_dir, data_dir
|
|
14
14
|
from .oldnews import OldNews
|
|
15
15
|
|
|
@@ -155,7 +155,6 @@ def main() -> None:
|
|
|
155
155
|
case "themes":
|
|
156
156
|
show_themes()
|
|
157
157
|
case _:
|
|
158
|
-
initialise_database()
|
|
159
158
|
OldNews(args).run()
|
|
160
159
|
|
|
161
160
|
|
oldnews/data/__init__.py
CHANGED
|
@@ -9,7 +9,6 @@ from .config import (
|
|
|
9
9
|
save_configuration,
|
|
10
10
|
update_configuration,
|
|
11
11
|
)
|
|
12
|
-
from .db import initialise_database
|
|
13
12
|
from .dump import data_dump
|
|
14
13
|
from .last_grab import last_grabbed_data_at, remember_we_last_grabbed_at
|
|
15
14
|
from .local_articles import (
|
|
@@ -24,6 +23,7 @@ from .local_articles import (
|
|
|
24
23
|
rename_folder_for_articles,
|
|
25
24
|
save_local_articles,
|
|
26
25
|
)
|
|
26
|
+
from .local_data import initialise_local_data, shutdown_local_data
|
|
27
27
|
from .local_folders import get_local_folders, save_local_folders
|
|
28
28
|
from .local_subscriptions import get_local_subscriptions, save_local_subscriptions
|
|
29
29
|
from .local_unread import LocalUnread, get_local_unread, total_unread
|
|
@@ -34,9 +34,8 @@ from .reset import reset_data
|
|
|
34
34
|
##############################################################################
|
|
35
35
|
# Exports.
|
|
36
36
|
__all__ = [
|
|
37
|
-
"Configuration",
|
|
38
|
-
"Log",
|
|
39
37
|
"clean_old_read_articles",
|
|
38
|
+
"Configuration",
|
|
40
39
|
"data_dump",
|
|
41
40
|
"get_auth_token",
|
|
42
41
|
"get_local_articles",
|
|
@@ -45,12 +44,13 @@ __all__ = [
|
|
|
45
44
|
"get_local_unread",
|
|
46
45
|
"get_navigation_state",
|
|
47
46
|
"get_unread_article_ids",
|
|
48
|
-
"
|
|
47
|
+
"initialise_local_data",
|
|
49
48
|
"last_grabbed_data_at",
|
|
50
49
|
"load_configuration",
|
|
51
|
-
"locally_mark_read",
|
|
52
50
|
"locally_mark_article_ids_read",
|
|
51
|
+
"locally_mark_read",
|
|
53
52
|
"LocalUnread",
|
|
53
|
+
"Log",
|
|
54
54
|
"move_subscription_articles",
|
|
55
55
|
"remember_we_last_grabbed_at",
|
|
56
56
|
"remove_folder_from_articles",
|
|
@@ -63,6 +63,7 @@ __all__ = [
|
|
|
63
63
|
"save_local_subscriptions",
|
|
64
64
|
"save_navigation_state",
|
|
65
65
|
"set_auth_token",
|
|
66
|
+
"shutdown_local_data",
|
|
66
67
|
"total_unread",
|
|
67
68
|
"update_configuration",
|
|
68
69
|
]
|
oldnews/data/config.py
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Python imports.
|
|
5
|
+
from collections.abc import Iterator
|
|
5
6
|
from contextlib import contextmanager
|
|
6
7
|
from dataclasses import asdict, dataclass, field
|
|
7
|
-
from functools import
|
|
8
|
+
from functools import cache
|
|
8
9
|
from json import dumps, loads
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Iterator
|
|
11
11
|
|
|
12
12
|
##############################################################################
|
|
13
13
|
# Local imports.
|
|
@@ -37,6 +37,9 @@ class Configuration:
|
|
|
37
37
|
startup_refresh_holdoff_period: float = 600
|
|
38
38
|
"""The number of seconds to wait before hitting TheOldReader again on startup."""
|
|
39
39
|
|
|
40
|
+
article_download_batch_size: int = 50
|
|
41
|
+
"""The batch size to use when downloading articles."""
|
|
42
|
+
|
|
40
43
|
|
|
41
44
|
##############################################################################
|
|
42
45
|
def configuration_file() -> Path:
|
|
@@ -66,7 +69,7 @@ def save_configuration(configuration: Configuration) -> Configuration:
|
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
##############################################################################
|
|
69
|
-
@
|
|
72
|
+
@cache
|
|
70
73
|
def load_configuration() -> Configuration:
|
|
71
74
|
"""Load the configuration.
|
|
72
75
|
|
oldnews/data/last_grab.py
CHANGED
|
@@ -2,39 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Python imports.
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
|
|
7
7
|
##############################################################################
|
|
8
|
-
#
|
|
9
|
-
from
|
|
8
|
+
# Tortoise imports.
|
|
9
|
+
from tortoise.transactions import in_transaction
|
|
10
10
|
|
|
11
11
|
##############################################################################
|
|
12
12
|
# Local imports.
|
|
13
|
-
from .
|
|
13
|
+
from .models import LastGrabbed
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
##############################################################################
|
|
17
|
-
|
|
18
|
-
"""Table that holds details of when data was last grabbed."""
|
|
19
|
-
|
|
20
|
-
at_time: datetime
|
|
21
|
-
"""The time at which data was last grabbed."""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
##############################################################################
|
|
25
|
-
def last_grabbed_data_at() -> datetime | None:
|
|
17
|
+
async def last_grabbed_data_at() -> datetime | None:
|
|
26
18
|
"""The time at which data was last grabbed.
|
|
27
19
|
|
|
28
20
|
Returns:
|
|
29
21
|
The time at which we last grabbed data, or `None` if we never have.
|
|
30
22
|
"""
|
|
31
|
-
if
|
|
32
|
-
return
|
|
23
|
+
if last_grabbed := await LastGrabbed.first():
|
|
24
|
+
return last_grabbed.at_time
|
|
33
25
|
return None
|
|
34
26
|
|
|
35
27
|
|
|
36
28
|
##############################################################################
|
|
37
|
-
def remember_we_last_grabbed_at(grab_time: datetime | None = None) -> None:
|
|
29
|
+
async def remember_we_last_grabbed_at(grab_time: datetime | None = None) -> None:
|
|
38
30
|
"""Remember the time we last grabbed data.
|
|
39
31
|
|
|
40
32
|
Args:
|
|
@@ -43,9 +35,9 @@ def remember_we_last_grabbed_at(grab_time: datetime | None = None) -> None:
|
|
|
43
35
|
Note:
|
|
44
36
|
If `grab_time` isn't supplied then it is recorded as now.
|
|
45
37
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
async with in_transaction():
|
|
39
|
+
await LastGrabbed.all().delete()
|
|
40
|
+
await LastGrabbed.create(at_time=grab_time or datetime.now(UTC))
|
|
49
41
|
|
|
50
42
|
|
|
51
43
|
### last_grab.py ends here
|
oldnews/data/local_articles.py
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Python imports.
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
6
7
|
from html import unescape
|
|
7
|
-
from typing import
|
|
8
|
+
from typing import cast
|
|
8
9
|
|
|
9
10
|
##############################################################################
|
|
10
11
|
# OldAS imports.
|
|
@@ -12,102 +13,17 @@ from oldas import Article, Articles, Folder, Folders, State, Subscription
|
|
|
12
13
|
from oldas.articles import Alternate, Alternates, Direction, Origin, Summary
|
|
13
14
|
|
|
14
15
|
##############################################################################
|
|
15
|
-
#
|
|
16
|
-
from
|
|
16
|
+
# Tortoise imports.
|
|
17
|
+
from tortoise.transactions import in_transaction
|
|
17
18
|
|
|
18
19
|
##############################################################################
|
|
19
20
|
# Local imports.
|
|
20
21
|
from .log import Log
|
|
21
|
-
from .
|
|
22
|
+
from .models import LocalArticle, LocalArticleAlternate, LocalArticleCategory
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
##############################################################################
|
|
25
|
-
|
|
26
|
-
"""A local copy of an article."""
|
|
27
|
-
|
|
28
|
-
article_id: TypedField[str]
|
|
29
|
-
"""The ID of the article."""
|
|
30
|
-
title: str
|
|
31
|
-
"""The title of the article."""
|
|
32
|
-
published: TypedField[datetime] = TypedField(datetime)
|
|
33
|
-
"""The time when the article was published."""
|
|
34
|
-
updated: TypedField[datetime] = TypedField(datetime)
|
|
35
|
-
"""The time when the article was updated."""
|
|
36
|
-
author: str
|
|
37
|
-
"""The author of the article."""
|
|
38
|
-
summary_direction: str
|
|
39
|
-
"""The direction for the text in the summary."""
|
|
40
|
-
summary_content: TypedField[str] = TypedField(str, type="text")
|
|
41
|
-
"""The content of the summary."""
|
|
42
|
-
origin_stream_id: str
|
|
43
|
-
"""The stream ID for the article's origin."""
|
|
44
|
-
origin_title: str
|
|
45
|
-
"""The title of the origin of the article."""
|
|
46
|
-
origin_html_url: str
|
|
47
|
-
"""The URL of the HTML of the origin of the article."""
|
|
48
|
-
categories = relationship(
|
|
49
|
-
list["LocalArticleCategory"],
|
|
50
|
-
condition=lambda article, category: cast(LocalArticle, article).id
|
|
51
|
-
== cast(LocalArticleCategory, category).article,
|
|
52
|
-
join="left",
|
|
53
|
-
)
|
|
54
|
-
"""The categories associated with this article."""
|
|
55
|
-
alternate = relationship(
|
|
56
|
-
list["LocalArticleAlternate"],
|
|
57
|
-
condition=lambda article, alternate: cast(LocalArticle, article).id
|
|
58
|
-
== cast(LocalArticleAlternate, alternate).article,
|
|
59
|
-
join="left",
|
|
60
|
-
)
|
|
61
|
-
"""The alternates for the article."""
|
|
62
|
-
|
|
63
|
-
def add_category(self, category: str | State) -> None:
|
|
64
|
-
"""Add a given category to the local article.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
category: The category to add.
|
|
68
|
-
"""
|
|
69
|
-
if not str(category) in self.categories:
|
|
70
|
-
LocalArticleCategory.insert(article=self.id, category=str(category))
|
|
71
|
-
commit(LocalArticleCategory)
|
|
72
|
-
|
|
73
|
-
def remove_category(self, category: str | State) -> None:
|
|
74
|
-
"""Remove a given category from the local article.
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
category: The category to add.
|
|
78
|
-
"""
|
|
79
|
-
if str(category) in self.categories:
|
|
80
|
-
LocalArticleCategory.where(
|
|
81
|
-
(LocalArticleCategory.article == self.id)
|
|
82
|
-
& (LocalArticleCategory.category == str(category))
|
|
83
|
-
).delete()
|
|
84
|
-
commit(LocalArticleCategory)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
##############################################################################
|
|
88
|
-
class LocalArticleCategory(TypedTable):
|
|
89
|
-
"""A local copy of the categories associated with an article."""
|
|
90
|
-
|
|
91
|
-
article: TypedField[LocalArticle]
|
|
92
|
-
"""The article that this category belongs to."""
|
|
93
|
-
category: str
|
|
94
|
-
"""The category."""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
##############################################################################
|
|
98
|
-
class LocalArticleAlternate(TypedTable):
|
|
99
|
-
"""A local copy of the alternate URLs associated with an article."""
|
|
100
|
-
|
|
101
|
-
article: TypedField[LocalArticle]
|
|
102
|
-
"""The article that this alternate belongs to."""
|
|
103
|
-
href: str
|
|
104
|
-
"""The URL of the alternate."""
|
|
105
|
-
mime_type: str
|
|
106
|
-
"""The MIME type of the alternate."""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
##############################################################################
|
|
110
|
-
def save_local_articles(articles: Articles) -> Articles:
|
|
26
|
+
async def save_local_articles(articles: Articles) -> Articles:
|
|
111
27
|
"""Locally save the given articles.
|
|
112
28
|
|
|
113
29
|
Args:
|
|
@@ -117,108 +33,55 @@ def save_local_articles(articles: Articles) -> Articles:
|
|
|
117
33
|
The articles.
|
|
118
34
|
"""
|
|
119
35
|
for article in articles:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
36
|
+
async with in_transaction():
|
|
37
|
+
local_article, _ = await LocalArticle.update_or_create(
|
|
38
|
+
article_id=article.id,
|
|
39
|
+
defaults={
|
|
40
|
+
"title": article.title,
|
|
41
|
+
"published": article.published,
|
|
42
|
+
"updated": article.updated,
|
|
43
|
+
"author": article.author,
|
|
44
|
+
"summary_direction": article.summary.direction,
|
|
45
|
+
"summary_content": article.summary.content,
|
|
46
|
+
"origin_stream_id": article.origin.stream_id,
|
|
47
|
+
"origin_title": article.origin.title,
|
|
48
|
+
"origin_html_url": article.origin.html_url,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
await LocalArticleCategory.filter(article=local_article).delete()
|
|
52
|
+
await LocalArticleCategory.bulk_create(
|
|
53
|
+
LocalArticleCategory(article=local_article, category=str(category))
|
|
137
54
|
for category in article.categories
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
"mime_type": alternate.mime_type,
|
|
147
|
-
}
|
|
55
|
+
)
|
|
56
|
+
await LocalArticleAlternate.filter(article=local_article).delete()
|
|
57
|
+
await LocalArticleAlternate.bulk_create(
|
|
58
|
+
LocalArticleAlternate(
|
|
59
|
+
article=local_article,
|
|
60
|
+
href=alternate.href,
|
|
61
|
+
mime_type=alternate.mime_type,
|
|
62
|
+
)
|
|
148
63
|
for alternate in article.alternate
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
commit(LocalArticle)
|
|
64
|
+
)
|
|
152
65
|
return articles
|
|
153
66
|
|
|
154
67
|
|
|
155
68
|
##############################################################################
|
|
156
|
-
def get_local_read_article_ids() -> set[
|
|
69
|
+
async def get_local_read_article_ids() -> set[str]:
|
|
157
70
|
"""Get the set of local articles that have been read.
|
|
158
71
|
|
|
159
72
|
Returns:
|
|
160
73
|
A `set` of IDs of articles that have been read.
|
|
161
74
|
"""
|
|
162
75
|
return {
|
|
163
|
-
category.article.
|
|
164
|
-
for category in LocalArticleCategory.
|
|
165
|
-
|
|
166
|
-
).
|
|
76
|
+
category.article.article_id
|
|
77
|
+
for category in await LocalArticleCategory.filter(
|
|
78
|
+
category=str(State.READ)
|
|
79
|
+
).prefetch_related("article")
|
|
167
80
|
}
|
|
168
81
|
|
|
169
82
|
|
|
170
83
|
##############################################################################
|
|
171
|
-
def
|
|
172
|
-
subscription: Subscription, unread_only: bool
|
|
173
|
-
) -> Iterator[LocalArticle]:
|
|
174
|
-
"""Get all unread articles for a given subscription.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
subscription: The subscription to get the articles for.
|
|
178
|
-
unread_only: Only load up the unread articles?
|
|
179
|
-
|
|
180
|
-
Yields:
|
|
181
|
-
The articles.
|
|
182
|
-
"""
|
|
183
|
-
read = get_local_read_article_ids() if unread_only else set()
|
|
184
|
-
for article in (
|
|
185
|
-
LocalArticle.where(~LocalArticle.id.belongs(read))
|
|
186
|
-
.where(origin_stream_id=subscription.id)
|
|
187
|
-
.join()
|
|
188
|
-
.orderby(~LocalArticle.published)
|
|
189
|
-
):
|
|
190
|
-
yield article
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
##############################################################################
|
|
194
|
-
def _for_folder(folder: Folder, unread_only: bool) -> Iterator[LocalArticle]:
|
|
195
|
-
"""Get all unread articles for a given folder.
|
|
196
|
-
|
|
197
|
-
Args:
|
|
198
|
-
folder: The folder to get the articles for.
|
|
199
|
-
unread_only: Only load up the unread articles?
|
|
200
|
-
|
|
201
|
-
Yields:
|
|
202
|
-
The unread articles.
|
|
203
|
-
"""
|
|
204
|
-
in_folder = {
|
|
205
|
-
category.article.id
|
|
206
|
-
for category in LocalArticleCategory.where(
|
|
207
|
-
LocalArticleCategory.category == folder.id
|
|
208
|
-
).collect()
|
|
209
|
-
}
|
|
210
|
-
read = get_local_read_article_ids() if unread_only else set()
|
|
211
|
-
for article in (
|
|
212
|
-
LocalArticle.where(LocalArticle.id.belongs(in_folder - read))
|
|
213
|
-
.select()
|
|
214
|
-
.join()
|
|
215
|
-
.orderby(~LocalArticle.published)
|
|
216
|
-
):
|
|
217
|
-
yield article
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
##############################################################################
|
|
221
|
-
def get_local_articles(
|
|
84
|
+
async def get_local_articles(
|
|
222
85
|
related_to: Folder | Subscription, unread_only: bool
|
|
223
86
|
) -> Articles:
|
|
224
87
|
"""Get all available unread articles.
|
|
@@ -229,12 +92,18 @@ def get_local_articles(
|
|
|
229
92
|
|
|
230
93
|
Returns: The unread articles.
|
|
231
94
|
"""
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
_for_folder(related_to, unread_only)
|
|
95
|
+
local_articles = (
|
|
96
|
+
LocalArticle.filter(categories__category=related_to.id)
|
|
235
97
|
if isinstance(related_to, Folder)
|
|
236
|
-
else
|
|
237
|
-
)
|
|
98
|
+
else LocalArticle.filter(origin_stream_id=related_to.id)
|
|
99
|
+
)
|
|
100
|
+
if unread_only and (read := (await get_local_read_article_ids())):
|
|
101
|
+
local_articles = local_articles.filter(article_id__not_in=read)
|
|
102
|
+
|
|
103
|
+
articles: list[Article] = []
|
|
104
|
+
for article in await local_articles.prefetch_related(
|
|
105
|
+
"categories", "alternates"
|
|
106
|
+
).order_by("-published"):
|
|
238
107
|
articles.append(
|
|
239
108
|
Article(
|
|
240
109
|
id=article.article_id,
|
|
@@ -243,11 +112,12 @@ def get_local_articles(
|
|
|
243
112
|
updated=article.updated,
|
|
244
113
|
author=article.author,
|
|
245
114
|
categories=Article.clean_categories(
|
|
246
|
-
category.category
|
|
115
|
+
category.category
|
|
116
|
+
for category in article.categories # type: ignore
|
|
247
117
|
),
|
|
248
118
|
alternate=Alternates(
|
|
249
119
|
Alternate(href=alternate.href, mime_type=alternate.mime_type)
|
|
250
|
-
for alternate in article.
|
|
120
|
+
for alternate in article.alternates # type: ignore
|
|
251
121
|
),
|
|
252
122
|
origin=Origin(
|
|
253
123
|
stream_id=article.origin_stream_id,
|
|
@@ -264,95 +134,93 @@ def get_local_articles(
|
|
|
264
134
|
|
|
265
135
|
|
|
266
136
|
##############################################################################
|
|
267
|
-
def locally_mark_read(article: Article) -> None:
|
|
137
|
+
async def locally_mark_read(article: Article) -> None:
|
|
268
138
|
"""Mark the given article as read.
|
|
269
139
|
|
|
270
140
|
Args:
|
|
271
141
|
article: The article to locally mark as read.
|
|
272
142
|
"""
|
|
273
|
-
if local_article := LocalArticle.
|
|
274
|
-
|
|
275
|
-
).first():
|
|
276
|
-
local_article.add_category(State.READ)
|
|
143
|
+
if local_article := await LocalArticle.filter(article_id=article.id).get_or_none():
|
|
144
|
+
await local_article.add_category(str(State.READ))
|
|
277
145
|
|
|
278
146
|
|
|
279
147
|
##############################################################################
|
|
280
|
-
def locally_mark_article_ids_read(articles: Iterable[str]) -> None:
|
|
148
|
+
async def locally_mark_article_ids_read(articles: Iterable[str]) -> None:
|
|
281
149
|
"""Locally mark a collection of article IDs as being read.
|
|
282
150
|
|
|
283
151
|
Args:
|
|
284
152
|
articles: The article IDs to mark as read.
|
|
285
153
|
"""
|
|
286
|
-
|
|
287
|
-
|
|
154
|
+
if article_ids := set(articles):
|
|
155
|
+
Log().debug(f"Number of articles to mark as read: {len(article_ids)}")
|
|
156
|
+
await LocalArticleCategory.bulk_create(
|
|
157
|
+
[
|
|
158
|
+
LocalArticleCategory(article=article, category=str(State.READ))
|
|
159
|
+
for article in await LocalArticle.filter(article_id__in=article_ids)
|
|
160
|
+
],
|
|
161
|
+
ignore_conflicts=True,
|
|
162
|
+
)
|
|
288
163
|
|
|
289
164
|
|
|
290
165
|
##############################################################################
|
|
291
|
-
def unread_count_in(
|
|
292
|
-
category: Folder | Subscription, read: set[
|
|
166
|
+
async def unread_count_in(
|
|
167
|
+
category: Folder | Subscription, read: set[str] | None = None
|
|
293
168
|
) -> int:
|
|
294
169
|
"""Get the count of unread articles in a given category.
|
|
295
170
|
|
|
296
171
|
Args:
|
|
297
|
-
category: The category to get the unread count for.
|
|
172
|
+
category: The category (Folder or Subscription) to get the unread count for.
|
|
298
173
|
read: The set of IDs of read articles.
|
|
299
174
|
|
|
300
175
|
Returns:
|
|
301
176
|
The count of unread articles in that category.
|
|
302
|
-
|
|
303
|
-
Notes:
|
|
304
|
-
Note that `read` is optional and will be worked out of not passed,
|
|
305
|
-
but if this function is being called in a tight loop it's more
|
|
306
|
-
efficient to provide this externally.
|
|
307
177
|
"""
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for category in LocalArticleCategory.where(
|
|
313
|
-
LocalArticleCategory.category == category.id
|
|
314
|
-
).collect()
|
|
315
|
-
}
|
|
316
|
-
return LocalArticle.where(LocalArticle.id.belongs(in_folder - read)).count()
|
|
317
|
-
return (
|
|
318
|
-
LocalArticle.where(~LocalArticle.id.belongs(read))
|
|
319
|
-
.where(origin_stream_id=category.id)
|
|
320
|
-
.count()
|
|
178
|
+
query = (
|
|
179
|
+
LocalArticle.filter(categories__category=category.id)
|
|
180
|
+
if isinstance(category, Folder)
|
|
181
|
+
else LocalArticle.filter(origin_stream_id=category.id)
|
|
321
182
|
)
|
|
183
|
+
if read := read if read is not None else await get_local_read_article_ids():
|
|
184
|
+
query = query.filter(article_id__not_in=read)
|
|
185
|
+
return await query.count()
|
|
322
186
|
|
|
323
187
|
|
|
324
188
|
##############################################################################
|
|
325
|
-
def get_unread_article_ids() -> list[str]:
|
|
189
|
+
async def get_unread_article_ids() -> list[str]:
|
|
326
190
|
"""Get a list of all the unread article IDs.
|
|
327
191
|
|
|
328
192
|
Returns:
|
|
329
193
|
The list of IDs of unread articles.
|
|
330
194
|
"""
|
|
331
|
-
read = get_local_read_article_ids()
|
|
195
|
+
read = await get_local_read_article_ids()
|
|
332
196
|
return [
|
|
333
197
|
article.article_id
|
|
334
|
-
for article in LocalArticle.
|
|
198
|
+
for article in await LocalArticle.filter(article_id__not_in=read)
|
|
335
199
|
]
|
|
336
200
|
|
|
337
201
|
|
|
338
202
|
##############################################################################
|
|
339
|
-
def clean_old_read_articles(cutoff: timedelta) -> int:
|
|
340
|
-
"""Clean up articles that are older than the given cutoff time.
|
|
341
|
-
|
|
342
|
-
|
|
203
|
+
async def clean_old_read_articles(cutoff: timedelta) -> int:
|
|
204
|
+
"""Clean up articles that are older than the given cutoff time.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
cutoff: The cutoff period after which articles will be removed.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The number of removed articles.
|
|
211
|
+
"""
|
|
212
|
+
read = await get_local_read_article_ids()
|
|
213
|
+
retire_time = datetime.now(UTC) - cutoff
|
|
343
214
|
Log().debug(f"Cleaning up read articles published before {retire_time}")
|
|
344
|
-
cleaned =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
).delete()
|
|
348
|
-
)
|
|
215
|
+
cleaned = await LocalArticle.filter(
|
|
216
|
+
published__lt=retire_time, article_id__in=read
|
|
217
|
+
).delete()
|
|
349
218
|
Log().debug(f"Cleaned: {cleaned}")
|
|
350
|
-
commit(LocalArticle)
|
|
351
219
|
return cleaned
|
|
352
220
|
|
|
353
221
|
|
|
354
222
|
##############################################################################
|
|
355
|
-
def rename_folder_for_articles(rename_from: str | Folder, rename_to: str) -> None:
|
|
223
|
+
async def rename_folder_for_articles(rename_from: str | Folder, rename_to: str) -> None:
|
|
356
224
|
"""Rename a folder for all articles that are in that folder.
|
|
357
225
|
|
|
358
226
|
Args:
|
|
@@ -362,14 +230,11 @@ def rename_folder_for_articles(rename_from: str | Folder, rename_to: str) -> Non
|
|
|
362
230
|
rename_from = Folders.full_id(rename_from)
|
|
363
231
|
rename_to = Folders.full_id(rename_to)
|
|
364
232
|
Log().debug(f"Renaming folder for local articles from {rename_from} to {rename_to}")
|
|
365
|
-
LocalArticleCategory.
|
|
366
|
-
category=rename_to
|
|
367
|
-
)
|
|
368
|
-
commit(LocalArticleCategory)
|
|
233
|
+
await LocalArticleCategory.filter(category=rename_from).update(category=rename_to)
|
|
369
234
|
|
|
370
235
|
|
|
371
236
|
##############################################################################
|
|
372
|
-
def remove_folder_from_articles(folder: str | Folder) -> None:
|
|
237
|
+
async def remove_folder_from_articles(folder: str | Folder) -> None:
|
|
373
238
|
"""Remove a folder from being associated with all articles.
|
|
374
239
|
|
|
375
240
|
Args:
|
|
@@ -377,12 +242,11 @@ def remove_folder_from_articles(folder: str | Folder) -> None:
|
|
|
377
242
|
"""
|
|
378
243
|
folder = Folders.full_id(folder)
|
|
379
244
|
Log().debug(f"Removing folder {folder} from all local articles")
|
|
380
|
-
LocalArticleCategory.
|
|
381
|
-
commit(LocalArticleCategory)
|
|
245
|
+
await LocalArticleCategory.filter(category=folder).delete()
|
|
382
246
|
|
|
383
247
|
|
|
384
248
|
##############################################################################
|
|
385
|
-
def move_subscription_articles(
|
|
249
|
+
async def move_subscription_articles(
|
|
386
250
|
subscription: Subscription,
|
|
387
251
|
from_folder: str | Folder | None,
|
|
388
252
|
to_folder: str | Folder | None,
|
|
@@ -401,20 +265,17 @@ def move_subscription_articles(
|
|
|
401
265
|
Log().debug(
|
|
402
266
|
f"Moving all articles of {subscription.title} ({subscription.id}) from folder {from_folder} to {to_folder}"
|
|
403
267
|
)
|
|
404
|
-
for article in LocalArticle.
|
|
268
|
+
for article in await LocalArticle.filter(
|
|
269
|
+
origin_stream_id=subscription.id
|
|
270
|
+
).prefetch_related("categories"):
|
|
405
271
|
if from_folder:
|
|
406
|
-
|
|
407
|
-
(LocalArticleCategory.article == article.id)
|
|
408
|
-
& (LocalArticleCategory.category == from_folder)
|
|
409
|
-
).delete()
|
|
410
|
-
commit(LocalArticleCategory)
|
|
272
|
+
await article.remove_category(from_folder)
|
|
411
273
|
if to_folder:
|
|
412
|
-
|
|
413
|
-
commit(LocalArticleCategory)
|
|
274
|
+
await article.add_category(to_folder)
|
|
414
275
|
|
|
415
276
|
|
|
416
277
|
##############################################################################
|
|
417
|
-
def remove_subscription_articles(subscription: str | Subscription) -> None:
|
|
278
|
+
async def remove_subscription_articles(subscription: str | Subscription) -> None:
|
|
418
279
|
"""Remove all the articles associated with the given subscription.
|
|
419
280
|
|
|
420
281
|
Args:
|
|
@@ -423,8 +284,8 @@ def remove_subscription_articles(subscription: str | Subscription) -> None:
|
|
|
423
284
|
if isinstance(subscription, Subscription):
|
|
424
285
|
subscription = subscription.id
|
|
425
286
|
Log().debug(f"Removing all local articles for subscription {subscription}")
|
|
426
|
-
LocalArticle.
|
|
427
|
-
|
|
287
|
+
deleted = await LocalArticle.filter(origin_stream_id=subscription).delete()
|
|
288
|
+
Log().debug(f"Articles removed that belonged to {subscription}: {deleted}")
|
|
428
289
|
|
|
429
290
|
|
|
430
291
|
### local_articles.py ends here
|