oldnews 0.8.0__tar.gz → 0.9.0__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.
- {oldnews-0.8.0 → oldnews-0.9.0}/PKG-INFO +2 -2
- {oldnews-0.8.0 → oldnews-0.9.0}/pyproject.toml +26 -3
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/__main__.py +1 -2
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/__init__.py +6 -5
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/config.py +6 -3
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/last_grab.py +11 -19
- oldnews-0.9.0/src/oldnews/data/local_articles.py +291 -0
- oldnews-0.9.0/src/oldnews/data/local_data.py +45 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/local_folders.py +12 -21
- oldnews-0.9.0/src/oldnews/data/local_subscriptions.py +79 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/local_unread.py +5 -3
- oldnews-0.9.0/src/oldnews/data/models/__init__.py +24 -0
- oldnews-0.9.0/src/oldnews/data/models/local_article.py +81 -0
- oldnews-0.9.0/src/oldnews/data/models/local_folder.py +19 -0
- oldnews-0.9.0/src/oldnews/data/models/local_state.py +25 -0
- oldnews-0.9.0/src/oldnews/data/models/local_subscription.py +41 -0
- oldnews-0.9.0/src/oldnews/data/navigation_state.py +36 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/reset.py +7 -1
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/oldnews.py +8 -1
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/screens/information_display.py +1 -1
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/screens/main.py +55 -47
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/sync.py +40 -35
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/widgets/_next_matching_option.py +1 -1
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/widgets/article_list.py +10 -9
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/widgets/navigation.py +24 -14
- oldnews-0.8.0/src/oldnews/data/db.py +0 -97
- oldnews-0.8.0/src/oldnews/data/local_articles.py +0 -430
- oldnews-0.8.0/src/oldnews/data/local_subscriptions.py +0 -120
- oldnews-0.8.0/src/oldnews/data/navigation_state.py +0 -45
- oldnews-0.8.0/src/oldnews/data/tools.py +0 -45
- {oldnews-0.8.0 → oldnews-0.9.0}/README.md +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/__init__.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/commands/__init__.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/commands/main.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/auth.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/dump.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/locations.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/data/log.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/providers/__init__.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/providers/main.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/py.typed +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/screens/__init__.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/screens/folder_input.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/screens/login.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/screens/new_subscription.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/screens/process_subscription.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/widgets/__init__.py +0 -0
- {oldnews-0.8.0 → oldnews-0.9.0}/src/oldnews/widgets/article_content.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oldnews
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A terminal-based client for TheOldReader
|
|
5
5
|
Keywords: atom,client,Google Reader,RSS,TheOldReader,terminal,news-reader,news
|
|
6
6
|
Author: Dave Pearson
|
|
@@ -20,7 +20,7 @@ Requires-Dist: oldas>=0.10.0
|
|
|
20
20
|
Requires-Dist: textual>=6.3.0
|
|
21
21
|
Requires-Dist: textual-autocomplete>=4.0.6
|
|
22
22
|
Requires-Dist: textual-enhanced>=1.2.0
|
|
23
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: tortoise-orm>=0.25.4
|
|
24
24
|
Requires-Dist: xdg-base-dirs>=6.0.2
|
|
25
25
|
Requires-Python: >=3.12
|
|
26
26
|
Project-URL: Homepage, https://github.com/davep/oldnews
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oldnews"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "A terminal-based client for TheOldReader"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
"textual>=6.3.0",
|
|
26
26
|
"textual-autocomplete>=4.0.6",
|
|
27
27
|
"textual-enhanced>=1.2.0",
|
|
28
|
-
"
|
|
28
|
+
"tortoise-orm>=0.25.4",
|
|
29
29
|
"xdg-base-dirs>=6.0.2",
|
|
30
30
|
]
|
|
31
31
|
classifiers = [
|
|
@@ -51,7 +51,7 @@ Discussions = "https://github.com/davep/oldnews/discussions"
|
|
|
51
51
|
oldnews = "oldnews.__main__:main"
|
|
52
52
|
|
|
53
53
|
[build-system]
|
|
54
|
-
requires = ["uv_build>=0.9.4,<0.
|
|
54
|
+
requires = ["uv_build>=0.9.4,<0.11.0"]
|
|
55
55
|
build-backend = "uv_build"
|
|
56
56
|
|
|
57
57
|
[[tool.uv.index]]
|
|
@@ -73,3 +73,26 @@ dev = [
|
|
|
73
73
|
"ruff>=0.14.0",
|
|
74
74
|
"textual-dev>=1.8.0",
|
|
75
75
|
]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = [
|
|
79
|
+
# pycodestyle
|
|
80
|
+
"E",
|
|
81
|
+
# Pyflakes
|
|
82
|
+
"F",
|
|
83
|
+
# pyupgrade
|
|
84
|
+
"UP",
|
|
85
|
+
# flake8-bugbear
|
|
86
|
+
"B",
|
|
87
|
+
# flake8-simplify
|
|
88
|
+
"SIM",
|
|
89
|
+
# isort
|
|
90
|
+
"I",
|
|
91
|
+
]
|
|
92
|
+
ignore = [
|
|
93
|
+
# I think try...expect...pass reads far better.
|
|
94
|
+
"SIM105",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.pycodestyle]
|
|
98
|
+
max-line-length = 120
|
|
@@ -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
|
|
|
@@ -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
|
]
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Code relating to persisting articles."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Python imports.
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
|
+
from html import unescape
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
##############################################################################
|
|
11
|
+
# OldAS imports.
|
|
12
|
+
from oldas import Article, Articles, Folder, Folders, State, Subscription
|
|
13
|
+
from oldas.articles import Alternate, Alternates, Direction, Origin, Summary
|
|
14
|
+
|
|
15
|
+
##############################################################################
|
|
16
|
+
# Tortoise imports.
|
|
17
|
+
from tortoise.transactions import in_transaction
|
|
18
|
+
|
|
19
|
+
##############################################################################
|
|
20
|
+
# Local imports.
|
|
21
|
+
from .log import Log
|
|
22
|
+
from .models import LocalArticle, LocalArticleAlternate, LocalArticleCategory
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
##############################################################################
|
|
26
|
+
async def save_local_articles(articles: Articles) -> Articles:
|
|
27
|
+
"""Locally save the given articles.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
articles: The articles to save.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The articles.
|
|
34
|
+
"""
|
|
35
|
+
for article in articles:
|
|
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))
|
|
54
|
+
for category in article.categories
|
|
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
|
+
)
|
|
63
|
+
for alternate in article.alternate
|
|
64
|
+
)
|
|
65
|
+
return articles
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
##############################################################################
|
|
69
|
+
async def get_local_read_article_ids() -> set[str]:
|
|
70
|
+
"""Get the set of local articles that have been read.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A `set` of IDs of articles that have been read.
|
|
74
|
+
"""
|
|
75
|
+
return {
|
|
76
|
+
category.article.article_id
|
|
77
|
+
for category in await LocalArticleCategory.filter(
|
|
78
|
+
category=str(State.READ)
|
|
79
|
+
).prefetch_related("article")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
##############################################################################
|
|
84
|
+
async def get_local_articles(
|
|
85
|
+
related_to: Folder | Subscription, unread_only: bool
|
|
86
|
+
) -> Articles:
|
|
87
|
+
"""Get all available unread articles.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
related_to: The folder or feed the articles should relate to.
|
|
91
|
+
unread_only: Only load up the unread articles?
|
|
92
|
+
|
|
93
|
+
Returns: The unread articles.
|
|
94
|
+
"""
|
|
95
|
+
local_articles = (
|
|
96
|
+
LocalArticle.filter(categories__category=related_to.id)
|
|
97
|
+
if isinstance(related_to, Folder)
|
|
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"):
|
|
107
|
+
articles.append(
|
|
108
|
+
Article(
|
|
109
|
+
id=article.article_id,
|
|
110
|
+
title=unescape(article.title),
|
|
111
|
+
published=article.published,
|
|
112
|
+
updated=article.updated,
|
|
113
|
+
author=article.author,
|
|
114
|
+
categories=Article.clean_categories(
|
|
115
|
+
category.category
|
|
116
|
+
for category in article.categories # type: ignore
|
|
117
|
+
),
|
|
118
|
+
alternate=Alternates(
|
|
119
|
+
Alternate(href=alternate.href, mime_type=alternate.mime_type)
|
|
120
|
+
for alternate in article.alternates # type: ignore
|
|
121
|
+
),
|
|
122
|
+
origin=Origin(
|
|
123
|
+
stream_id=article.origin_stream_id,
|
|
124
|
+
title=unescape(article.origin_title),
|
|
125
|
+
html_url=article.origin_html_url,
|
|
126
|
+
),
|
|
127
|
+
summary=Summary(
|
|
128
|
+
direction=cast(Direction, article.summary_direction),
|
|
129
|
+
content=article.summary_content,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
return Articles(articles)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
##############################################################################
|
|
137
|
+
async def locally_mark_read(article: Article) -> None:
|
|
138
|
+
"""Mark the given article as read.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
article: The article to locally mark as read.
|
|
142
|
+
"""
|
|
143
|
+
if local_article := await LocalArticle.filter(article_id=article.id).get_or_none():
|
|
144
|
+
await local_article.add_category(str(State.READ))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
##############################################################################
|
|
148
|
+
async def locally_mark_article_ids_read(articles: Iterable[str]) -> None:
|
|
149
|
+
"""Locally mark a collection of article IDs as being read.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
articles: The article IDs to mark as read.
|
|
153
|
+
"""
|
|
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
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
##############################################################################
|
|
166
|
+
async def unread_count_in(
|
|
167
|
+
category: Folder | Subscription, read: set[str] | None = None
|
|
168
|
+
) -> int:
|
|
169
|
+
"""Get the count of unread articles in a given category.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
category: The category (Folder or Subscription) to get the unread count for.
|
|
173
|
+
read: The set of IDs of read articles.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The count of unread articles in that category.
|
|
177
|
+
"""
|
|
178
|
+
query = (
|
|
179
|
+
LocalArticle.filter(categories__category=category.id)
|
|
180
|
+
if isinstance(category, Folder)
|
|
181
|
+
else LocalArticle.filter(origin_stream_id=category.id)
|
|
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()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
##############################################################################
|
|
189
|
+
async def get_unread_article_ids() -> list[str]:
|
|
190
|
+
"""Get a list of all the unread article IDs.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The list of IDs of unread articles.
|
|
194
|
+
"""
|
|
195
|
+
read = await get_local_read_article_ids()
|
|
196
|
+
return [
|
|
197
|
+
article.article_id
|
|
198
|
+
for article in await LocalArticle.filter(article_id__not_in=read)
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
##############################################################################
|
|
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
|
|
214
|
+
Log().debug(f"Cleaning up read articles published before {retire_time}")
|
|
215
|
+
cleaned = await LocalArticle.filter(
|
|
216
|
+
published__lt=retire_time, article_id__in=read
|
|
217
|
+
).delete()
|
|
218
|
+
Log().debug(f"Cleaned: {cleaned}")
|
|
219
|
+
return cleaned
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
##############################################################################
|
|
223
|
+
async def rename_folder_for_articles(rename_from: str | Folder, rename_to: str) -> None:
|
|
224
|
+
"""Rename a folder for all articles that are in that folder.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
rename_from: The folder name to rename from.
|
|
228
|
+
rename_to: The folder name to rename to.
|
|
229
|
+
"""
|
|
230
|
+
rename_from = Folders.full_id(rename_from)
|
|
231
|
+
rename_to = Folders.full_id(rename_to)
|
|
232
|
+
Log().debug(f"Renaming folder for local articles from {rename_from} to {rename_to}")
|
|
233
|
+
await LocalArticleCategory.filter(category=rename_from).update(category=rename_to)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
##############################################################################
|
|
237
|
+
async def remove_folder_from_articles(folder: str | Folder) -> None:
|
|
238
|
+
"""Remove a folder from being associated with all articles.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
folder: The folder to remove from all articles.
|
|
242
|
+
"""
|
|
243
|
+
folder = Folders.full_id(folder)
|
|
244
|
+
Log().debug(f"Removing folder {folder} from all local articles")
|
|
245
|
+
await LocalArticleCategory.filter(category=folder).delete()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
##############################################################################
|
|
249
|
+
async def move_subscription_articles(
|
|
250
|
+
subscription: Subscription,
|
|
251
|
+
from_folder: str | Folder | None,
|
|
252
|
+
to_folder: str | Folder | None,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Move the articles of a subscription from one folder to another.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
subscription: The subscription whose articles we should move.
|
|
258
|
+
from_folder: The folder to move from.
|
|
259
|
+
to_folder: The folder to move to.
|
|
260
|
+
"""
|
|
261
|
+
from_folder = (
|
|
262
|
+
Folders.full_id(from_folder) if from_folder is not None else from_folder
|
|
263
|
+
)
|
|
264
|
+
to_folder = Folders.full_id(to_folder) if to_folder is not None else to_folder
|
|
265
|
+
Log().debug(
|
|
266
|
+
f"Moving all articles of {subscription.title} ({subscription.id}) from folder {from_folder} to {to_folder}"
|
|
267
|
+
)
|
|
268
|
+
for article in await LocalArticle.filter(
|
|
269
|
+
origin_stream_id=subscription.id
|
|
270
|
+
).prefetch_related("categories"):
|
|
271
|
+
if from_folder:
|
|
272
|
+
await article.remove_category(from_folder)
|
|
273
|
+
if to_folder:
|
|
274
|
+
await article.add_category(to_folder)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
##############################################################################
|
|
278
|
+
async def remove_subscription_articles(subscription: str | Subscription) -> None:
|
|
279
|
+
"""Remove all the articles associated with the given subscription.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
subscription: The subscription to remove the articles for.
|
|
283
|
+
"""
|
|
284
|
+
if isinstance(subscription, Subscription):
|
|
285
|
+
subscription = subscription.id
|
|
286
|
+
Log().debug(f"Removing all local articles for subscription {subscription}")
|
|
287
|
+
deleted = await LocalArticle.filter(origin_stream_id=subscription).delete()
|
|
288
|
+
Log().debug(f"Articles removed that belonged to {subscription}: {deleted}")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
### local_articles.py ends here
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Provides code for setting up the local database."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Python imports.
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
##############################################################################
|
|
8
|
+
# Tortoise imports.
|
|
9
|
+
from tortoise import Tortoise
|
|
10
|
+
|
|
11
|
+
##############################################################################
|
|
12
|
+
# Local imports.
|
|
13
|
+
from .locations import data_dir
|
|
14
|
+
from .log import Log
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
##############################################################################
|
|
18
|
+
def local_db_file() -> Path:
|
|
19
|
+
"""Get the file that contains the database.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The file that contains the database.
|
|
23
|
+
"""
|
|
24
|
+
return data_dir() / "oldnews.db"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
##############################################################################
|
|
28
|
+
async def initialise_local_data() -> None:
|
|
29
|
+
"""Initialise the local storage."""
|
|
30
|
+
Log().debug("Database startup")
|
|
31
|
+
await Tortoise.init(
|
|
32
|
+
db_url=f"sqlite://{local_db_file()}",
|
|
33
|
+
modules={"models": ["oldnews.data.models"]},
|
|
34
|
+
)
|
|
35
|
+
await Tortoise.generate_schemas()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
##############################################################################
|
|
39
|
+
async def shutdown_local_data() -> None:
|
|
40
|
+
"""Close down the local connection."""
|
|
41
|
+
await Tortoise.close_connections()
|
|
42
|
+
Log().debug("Database shutdown")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### local_data.py ends here
|
|
@@ -5,26 +5,16 @@
|
|
|
5
5
|
from oldas import Folder, Folders
|
|
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 LocalFolder
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
##############################################################################
|
|
17
|
-
|
|
18
|
-
"""A local copy of a folder."""
|
|
19
|
-
|
|
20
|
-
folder_id: str
|
|
21
|
-
"""The ID of the folder."""
|
|
22
|
-
sort_id: str
|
|
23
|
-
"""The sort ID of the folder."""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
##############################################################################
|
|
27
|
-
def get_local_folders() -> Folders:
|
|
17
|
+
async def get_local_folders() -> Folders:
|
|
28
18
|
"""Gets the local cache of known folders.
|
|
29
19
|
|
|
30
20
|
Returns:
|
|
@@ -32,12 +22,12 @@ def get_local_folders() -> Folders:
|
|
|
32
22
|
"""
|
|
33
23
|
return Folders(
|
|
34
24
|
Folder(id=folder.folder_id, sort_id=folder.sort_id)
|
|
35
|
-
for folder in LocalFolder.
|
|
25
|
+
for folder in await LocalFolder.all()
|
|
36
26
|
)
|
|
37
27
|
|
|
38
28
|
|
|
39
29
|
##############################################################################
|
|
40
|
-
def save_local_folders(folders: Folders) -> Folders:
|
|
30
|
+
async def save_local_folders(folders: Folders) -> Folders:
|
|
41
31
|
"""Save the local copy of the known folders.
|
|
42
32
|
|
|
43
33
|
Args:
|
|
@@ -46,11 +36,12 @@ def save_local_folders(folders: Folders) -> Folders:
|
|
|
46
36
|
Returns:
|
|
47
37
|
The folders.
|
|
48
38
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
async with in_transaction():
|
|
40
|
+
await LocalFolder.all().delete()
|
|
41
|
+
await LocalFolder.bulk_create(
|
|
42
|
+
LocalFolder(folder_id=folder.id, sort_id=folder.sort_id)
|
|
43
|
+
for folder in folders
|
|
44
|
+
)
|
|
54
45
|
return folders
|
|
55
46
|
|
|
56
47
|
|