oldnews 0.4.0__tar.gz → 0.6.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.4.0 → oldnews-0.6.0}/PKG-INFO +3 -2
- {oldnews-0.4.0 → oldnews-0.6.0}/pyproject.toml +3 -2
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/__main__.py +11 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/commands/__init__.py +10 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/commands/main.py +35 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/__init__.py +12 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/db.py +9 -36
- oldnews-0.6.0/src/oldnews/data/dump.py +75 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/last_grab.py +5 -2
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/local_articles.py +88 -9
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/local_folders.py +5 -2
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/local_subscriptions.py +31 -23
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/local_unread.py +2 -2
- oldnews-0.6.0/src/oldnews/data/log.py +38 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/navigation_state.py +5 -2
- oldnews-0.6.0/src/oldnews/data/tools.py +45 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/providers/main.py +21 -11
- oldnews-0.6.0/src/oldnews/screens/folder_input.py +62 -0
- oldnews-0.6.0/src/oldnews/screens/information_display.py +86 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/screens/login.py +7 -3
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/screens/main.py +239 -45
- oldnews-0.6.0/src/oldnews/screens/new_subscription.py +128 -0
- oldnews-0.6.0/src/oldnews/sync.py +289 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/widgets/article_content.py +20 -9
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/widgets/navigation.py +24 -1
- oldnews-0.4.0/src/oldnews/sync.py +0 -158
- {oldnews-0.4.0 → oldnews-0.6.0}/README.md +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/__init__.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/auth.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/config.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/locations.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/data/reset.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/oldnews.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/providers/__init__.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/py.typed +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/screens/__init__.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/widgets/__init__.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/widgets/_after_highlight.py +0 -0
- {oldnews-0.4.0 → oldnews-0.6.0}/src/oldnews/widgets/article_list.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oldnews
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -15,8 +15,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
17
|
Requires-Dist: html-to-markdown>=2.15.0
|
|
18
|
-
Requires-Dist: oldas>=0.
|
|
18
|
+
Requires-Dist: oldas>=0.6.0
|
|
19
19
|
Requires-Dist: textual>=6.3.0
|
|
20
|
+
Requires-Dist: textual-autocomplete>=4.0.6
|
|
20
21
|
Requires-Dist: textual-enhanced>=1.2.0
|
|
21
22
|
Requires-Dist: typedal>=4.2.2
|
|
22
23
|
Requires-Dist: xdg-base-dirs>=6.0.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oldnews"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "A terminal-based client for TheOldReader"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -20,8 +20,9 @@ keywords = [
|
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
22
|
"html-to-markdown>=2.15.0",
|
|
23
|
-
"oldas>=0.
|
|
23
|
+
"oldas>=0.6.0",
|
|
24
24
|
"textual>=6.3.0",
|
|
25
|
+
"textual-autocomplete>=4.0.6",
|
|
25
26
|
"textual-enhanced>=1.2.0",
|
|
26
27
|
"typedal>=4.2.2",
|
|
27
28
|
"xdg-base-dirs>=6.0.2",
|
|
@@ -10,6 +10,7 @@ from operator import attrgetter
|
|
|
10
10
|
# Local imports.
|
|
11
11
|
from . import __doc__, __version__
|
|
12
12
|
from .data import initialise_database, reset_data
|
|
13
|
+
from .data.locations import config_dir, data_dir
|
|
13
14
|
from .oldnews import OldNews
|
|
14
15
|
|
|
15
16
|
|
|
@@ -49,6 +50,13 @@ def get_args() -> Namespace:
|
|
|
49
50
|
dest="command", help="Available commands", required=False
|
|
50
51
|
)
|
|
51
52
|
|
|
53
|
+
# Add the 'directories' command.
|
|
54
|
+
sub_parser.add_parser(
|
|
55
|
+
"directories",
|
|
56
|
+
aliases=["dirs", "d"],
|
|
57
|
+
help="Show the directories created and used by OldNews",
|
|
58
|
+
)
|
|
59
|
+
|
|
52
60
|
# Add the 'license' command.
|
|
53
61
|
sub_parser.add_parser(
|
|
54
62
|
"license",
|
|
@@ -135,6 +143,9 @@ def reset_news(args: Namespace) -> None:
|
|
|
135
143
|
def main() -> None:
|
|
136
144
|
"""Main entry function."""
|
|
137
145
|
match (args := get_args()).command:
|
|
146
|
+
case "d" | "dirs" | "directories":
|
|
147
|
+
print(config_dir())
|
|
148
|
+
print(data_dir())
|
|
138
149
|
case "reset":
|
|
139
150
|
reset_news(args)
|
|
140
151
|
case "license" | "licence":
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Local imports.
|
|
5
5
|
from .main import (
|
|
6
|
+
AddSubscription,
|
|
6
7
|
Copy,
|
|
7
8
|
CopyArticleToClipboard,
|
|
8
9
|
CopyFeedToClipboard,
|
|
9
10
|
CopyHomePageToClipboard,
|
|
10
11
|
Escape,
|
|
12
|
+
Information,
|
|
11
13
|
MarkAllRead,
|
|
14
|
+
MoveSubscription,
|
|
12
15
|
Next,
|
|
13
16
|
NextUnread,
|
|
14
17
|
OpenArticle,
|
|
@@ -16,25 +19,32 @@ from .main import (
|
|
|
16
19
|
Previous,
|
|
17
20
|
PreviousUnread,
|
|
18
21
|
RefreshFromTheOldReader,
|
|
22
|
+
Remove,
|
|
23
|
+
Rename,
|
|
19
24
|
ToggleShowAll,
|
|
20
25
|
)
|
|
21
26
|
|
|
22
27
|
##############################################################################
|
|
23
28
|
# Exports.
|
|
24
29
|
__all__ = [
|
|
30
|
+
"AddSubscription",
|
|
25
31
|
"Copy",
|
|
26
32
|
"CopyArticleToClipboard",
|
|
27
33
|
"CopyFeedToClipboard",
|
|
28
34
|
"CopyHomePageToClipboard",
|
|
29
35
|
"Escape",
|
|
36
|
+
"Information",
|
|
30
37
|
"MarkAllRead",
|
|
31
38
|
"Next",
|
|
32
39
|
"NextUnread",
|
|
40
|
+
"MoveSubscription",
|
|
33
41
|
"OpenArticle",
|
|
34
42
|
"OpenHomePage",
|
|
35
43
|
"Previous",
|
|
36
44
|
"PreviousUnread",
|
|
37
45
|
"RefreshFromTheOldReader",
|
|
46
|
+
"Rename",
|
|
47
|
+
"Remove",
|
|
38
48
|
"ToggleShowAll",
|
|
39
49
|
]
|
|
40
50
|
|
|
@@ -105,4 +105,39 @@ class Copy(Command):
|
|
|
105
105
|
BINDING_KEY = "ctrl+c"
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
##############################################################################
|
|
109
|
+
class AddSubscription(Command):
|
|
110
|
+
"""Add a subscription feed"""
|
|
111
|
+
|
|
112
|
+
BINDING_KEY = "plus"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
##############################################################################
|
|
116
|
+
class Rename(Command):
|
|
117
|
+
"""Rename the current folder or subscription"""
|
|
118
|
+
|
|
119
|
+
BINDING_KEY = "f6"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
##############################################################################
|
|
123
|
+
class Remove(Command):
|
|
124
|
+
"""Remove the current folder or subscription"""
|
|
125
|
+
|
|
126
|
+
BINDING_KEY = "delete"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
##############################################################################
|
|
130
|
+
class MoveSubscription(Command):
|
|
131
|
+
"""Move the current subscription to folder"""
|
|
132
|
+
|
|
133
|
+
BINDING_KEY = "f7"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
##############################################################################
|
|
137
|
+
class Information(Command):
|
|
138
|
+
"""Show low-level information about the selected item"""
|
|
139
|
+
|
|
140
|
+
BINDING_KEY = "f8"
|
|
141
|
+
|
|
142
|
+
|
|
108
143
|
### main.py ends here
|
|
@@ -10,6 +10,7 @@ from .config import (
|
|
|
10
10
|
update_configuration,
|
|
11
11
|
)
|
|
12
12
|
from .db import initialise_database
|
|
13
|
+
from .dump import data_dump
|
|
13
14
|
from .last_grab import last_grabbed_data_at, remember_we_last_grabbed_at
|
|
14
15
|
from .local_articles import (
|
|
15
16
|
clean_old_read_articles,
|
|
@@ -17,11 +18,16 @@ from .local_articles import (
|
|
|
17
18
|
get_unread_article_ids,
|
|
18
19
|
locally_mark_article_ids_read,
|
|
19
20
|
locally_mark_read,
|
|
21
|
+
move_subscription_articles,
|
|
22
|
+
remove_folder_from_articles,
|
|
23
|
+
remove_subscription_articles,
|
|
24
|
+
rename_folder_for_articles,
|
|
20
25
|
save_local_articles,
|
|
21
26
|
)
|
|
22
27
|
from .local_folders import get_local_folders, save_local_folders
|
|
23
28
|
from .local_subscriptions import get_local_subscriptions, save_local_subscriptions
|
|
24
29
|
from .local_unread import LocalUnread, get_local_unread, total_unread
|
|
30
|
+
from .log import Log
|
|
25
31
|
from .navigation_state import get_navigation_state, save_navigation_state
|
|
26
32
|
from .reset import reset_data
|
|
27
33
|
|
|
@@ -29,7 +35,9 @@ from .reset import reset_data
|
|
|
29
35
|
# Exports.
|
|
30
36
|
__all__ = [
|
|
31
37
|
"Configuration",
|
|
38
|
+
"Log",
|
|
32
39
|
"clean_old_read_articles",
|
|
40
|
+
"data_dump",
|
|
33
41
|
"get_auth_token",
|
|
34
42
|
"get_local_articles",
|
|
35
43
|
"get_local_folders",
|
|
@@ -43,7 +51,11 @@ __all__ = [
|
|
|
43
51
|
"locally_mark_read",
|
|
44
52
|
"locally_mark_article_ids_read",
|
|
45
53
|
"LocalUnread",
|
|
54
|
+
"move_subscription_articles",
|
|
46
55
|
"remember_we_last_grabbed_at",
|
|
56
|
+
"remove_folder_from_articles",
|
|
57
|
+
"remove_subscription_articles",
|
|
58
|
+
"rename_folder_for_articles",
|
|
47
59
|
"reset_data",
|
|
48
60
|
"save_configuration",
|
|
49
61
|
"save_local_articles",
|
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Python imports.
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
6
|
|
|
8
7
|
##############################################################################
|
|
9
8
|
# TypeDAL imports.
|
|
10
|
-
from typedal import TypeDAL
|
|
9
|
+
from typedal import TypeDAL
|
|
11
10
|
from typedal.config import TypeDALConfig
|
|
12
|
-
from typedal.helpers import get_field
|
|
13
|
-
from typedal.types import Field
|
|
14
11
|
|
|
15
12
|
##############################################################################
|
|
16
13
|
# Local imports.
|
|
@@ -20,6 +17,7 @@ from .local_folders import LocalFolder
|
|
|
20
17
|
from .local_subscriptions import LocalSubscription, LocalSubscriptionCategory
|
|
21
18
|
from .locations import data_dir
|
|
22
19
|
from .navigation_state import NavigationState
|
|
20
|
+
from .tools import safely_index
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
##############################################################################
|
|
@@ -32,31 +30,6 @@ def db_file() -> Path:
|
|
|
32
30
|
return data_dir() / "oldnews.db"
|
|
33
31
|
|
|
34
32
|
|
|
35
|
-
##############################################################################
|
|
36
|
-
def _safely_index(
|
|
37
|
-
table: type[TypedTable], name: str, field: str | Field | TypedField[Any]
|
|
38
|
-
) -> None:
|
|
39
|
-
"""Create an index on a type, but handle errors.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
table: The table to create the index against.
|
|
43
|
-
name: The name of the index.
|
|
44
|
-
field: The field to index.
|
|
45
|
-
|
|
46
|
-
Notes:
|
|
47
|
-
From what I can gather TypeDAL *should* only create the index if it
|
|
48
|
-
doesn't exist. Instead it throws an error if it exists. So here I
|
|
49
|
-
swallow the `RuntimeError`. Hopefully there is a better way and I've
|
|
50
|
-
just missed it.
|
|
51
|
-
"""
|
|
52
|
-
try:
|
|
53
|
-
table.create_index(
|
|
54
|
-
name, get_field(field) if isinstance(field, TypedField) else field
|
|
55
|
-
)
|
|
56
|
-
except RuntimeError:
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
|
|
60
33
|
##############################################################################
|
|
61
34
|
def initialise_database() -> TypeDAL:
|
|
62
35
|
"""Create the database.
|
|
@@ -73,20 +46,20 @@ def initialise_database() -> TypeDAL:
|
|
|
73
46
|
dal = TypeDAL(f"sqlite://{db_file()}", folder=data_dir(), config=TypeDALConfig())
|
|
74
47
|
|
|
75
48
|
dal.define(LocalArticle)
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
safely_index(LocalArticle, "idx_local_article_article_id", LocalArticle.article_id)
|
|
50
|
+
safely_index(
|
|
78
51
|
LocalArticle,
|
|
79
52
|
"idx_local_article_origin_stream_id",
|
|
80
53
|
LocalArticle.origin_stream_id,
|
|
81
54
|
)
|
|
82
55
|
|
|
83
56
|
dal.define(LocalArticleCategory)
|
|
84
|
-
|
|
57
|
+
safely_index(
|
|
85
58
|
LocalArticleCategory,
|
|
86
59
|
"idx_local_article_category_article",
|
|
87
60
|
LocalArticleCategory.article,
|
|
88
61
|
)
|
|
89
|
-
|
|
62
|
+
safely_index(
|
|
90
63
|
LocalArticleCategory,
|
|
91
64
|
"idx_local_article_category_category",
|
|
92
65
|
LocalArticleCategory.category,
|
|
@@ -97,19 +70,19 @@ def initialise_database() -> TypeDAL:
|
|
|
97
70
|
dal.define(LocalFolder)
|
|
98
71
|
|
|
99
72
|
dal.define(LocalSubscription)
|
|
100
|
-
|
|
73
|
+
safely_index(
|
|
101
74
|
LocalSubscription,
|
|
102
75
|
"idx_local_subscription_subscription_id",
|
|
103
76
|
LocalSubscription.subscription_id,
|
|
104
77
|
)
|
|
105
78
|
|
|
106
79
|
dal.define(LocalSubscriptionCategory)
|
|
107
|
-
|
|
80
|
+
safely_index(
|
|
108
81
|
LocalSubscriptionCategory,
|
|
109
82
|
"idx_local_subscription_category_subscription",
|
|
110
83
|
LocalSubscriptionCategory.subscription,
|
|
111
84
|
)
|
|
112
|
-
|
|
85
|
+
safely_index(
|
|
113
86
|
LocalSubscriptionCategory,
|
|
114
87
|
"idx_local_subscription_category_category_id",
|
|
115
88
|
LocalSubscriptionCategory.category_id,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Provides tools to dump data into an easy-to-browse format."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Python imports.
|
|
5
|
+
from functools import singledispatch
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
##############################################################################
|
|
9
|
+
# OldAS imports.
|
|
10
|
+
from oldas import Article, Folder, Subscription
|
|
11
|
+
|
|
12
|
+
##############################################################################
|
|
13
|
+
type DataDump = tuple[tuple[str, str], ...]
|
|
14
|
+
"""Type of a data dump."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
##############################################################################
|
|
18
|
+
@singledispatch
|
|
19
|
+
def data_dump(data: Any) -> DataDump:
|
|
20
|
+
"""Dump the given data into an easy-to-browse format.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
data: The data to dump.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A `DataDump` of the data.
|
|
27
|
+
"""
|
|
28
|
+
return (("Data", str(data)),)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
##############################################################################
|
|
32
|
+
@data_dump.register
|
|
33
|
+
def _(data: Folder) -> DataDump:
|
|
34
|
+
return (("ID", data.id), ("Sort ID", data.sort_id))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
##############################################################################
|
|
38
|
+
@data_dump.register
|
|
39
|
+
def _(data: Subscription) -> DataDump:
|
|
40
|
+
return (
|
|
41
|
+
("ID", data.id),
|
|
42
|
+
("Title", data.title),
|
|
43
|
+
("Sort ID", data.sort_id),
|
|
44
|
+
("First Item Time", f"{data.first_item_time}"),
|
|
45
|
+
("URL", data.url),
|
|
46
|
+
("HTML URL", data.html_url),
|
|
47
|
+
*(
|
|
48
|
+
(
|
|
49
|
+
f"Category[{n}]",
|
|
50
|
+
f"{category.id}, {category.label}",
|
|
51
|
+
)
|
|
52
|
+
for n, category in enumerate(data.categories)
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
##############################################################################
|
|
58
|
+
@data_dump.register
|
|
59
|
+
def _(data: Article) -> DataDump:
|
|
60
|
+
# TODO: The article has pretty rich data, so in here I'm not showing
|
|
61
|
+
# it all, just enough to be useful. In the future perhaps make it a
|
|
62
|
+
# lot richer.
|
|
63
|
+
return (
|
|
64
|
+
("ID", data.id),
|
|
65
|
+
("Title", data.title),
|
|
66
|
+
("Published", f"{data.published}"),
|
|
67
|
+
("Updated", f"{data.updated}"),
|
|
68
|
+
*(
|
|
69
|
+
(f"Category[{n}]", f"{category}")
|
|
70
|
+
for n, category in enumerate(data.categories)
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### dump.py ends here
|
|
@@ -8,6 +8,10 @@ from datetime import datetime, timezone
|
|
|
8
8
|
# TypeDAL imports.
|
|
9
9
|
from typedal import TypedTable
|
|
10
10
|
|
|
11
|
+
##############################################################################
|
|
12
|
+
# Local imports.
|
|
13
|
+
from .tools import commit
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
##############################################################################
|
|
13
17
|
class LastGrabbed(TypedTable):
|
|
@@ -39,10 +43,9 @@ def remember_we_last_grabbed_at(grab_time: datetime | None = None) -> None:
|
|
|
39
43
|
Note:
|
|
40
44
|
If `grab_time` isn't supplied then it is recorded as now.
|
|
41
45
|
"""
|
|
42
|
-
assert LastGrabbed._db is not None
|
|
43
46
|
LastGrabbed.truncate()
|
|
44
47
|
LastGrabbed.insert(at_time=grab_time or datetime.now(timezone.utc))
|
|
45
|
-
|
|
48
|
+
commit(LastGrabbed)
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
### last_grab.py ends here
|
|
@@ -8,13 +8,18 @@ from typing import Iterable, Iterator, cast
|
|
|
8
8
|
|
|
9
9
|
##############################################################################
|
|
10
10
|
# OldAS imports.
|
|
11
|
-
from oldas import Article, Articles, Folder, State, Subscription
|
|
11
|
+
from oldas import Article, Articles, Folder, Folders, State, Subscription
|
|
12
12
|
from oldas.articles import Alternate, Alternates, Direction, Origin, Summary
|
|
13
13
|
|
|
14
14
|
##############################################################################
|
|
15
15
|
# TypeDAL imports.
|
|
16
16
|
from typedal import TypedField, TypedTable, relationship
|
|
17
17
|
|
|
18
|
+
##############################################################################
|
|
19
|
+
# Local imports.
|
|
20
|
+
from .log import Log
|
|
21
|
+
from .tools import commit
|
|
22
|
+
|
|
18
23
|
|
|
19
24
|
##############################################################################
|
|
20
25
|
class LocalArticle(TypedTable):
|
|
@@ -62,9 +67,8 @@ class LocalArticle(TypedTable):
|
|
|
62
67
|
category: The category to add.
|
|
63
68
|
"""
|
|
64
69
|
if not str(category) in self.categories:
|
|
65
|
-
assert LocalArticleCategory._db is not None
|
|
66
70
|
LocalArticleCategory.insert(article=self.id, category=str(category))
|
|
67
|
-
|
|
71
|
+
commit(LocalArticleCategory)
|
|
68
72
|
|
|
69
73
|
def remove_category(self, category: str | State) -> None:
|
|
70
74
|
"""Remove a given category from the local article.
|
|
@@ -73,12 +77,11 @@ class LocalArticle(TypedTable):
|
|
|
73
77
|
category: The category to add.
|
|
74
78
|
"""
|
|
75
79
|
if str(category) in self.categories:
|
|
76
|
-
assert LocalArticleCategory._db is not None
|
|
77
80
|
LocalArticleCategory.where(
|
|
78
81
|
(LocalArticleCategory.article == self.id)
|
|
79
82
|
& (LocalArticleCategory.category == str(category))
|
|
80
83
|
).delete()
|
|
81
|
-
|
|
84
|
+
commit(LocalArticleCategory)
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
##############################################################################
|
|
@@ -113,7 +116,6 @@ def save_local_articles(articles: Articles) -> Articles:
|
|
|
113
116
|
Returns:
|
|
114
117
|
The articles.
|
|
115
118
|
"""
|
|
116
|
-
assert LocalArticle._db is not None
|
|
117
119
|
for article in articles:
|
|
118
120
|
local_article = LocalArticle.update_or_insert(
|
|
119
121
|
LocalArticle.article_id == article.id,
|
|
@@ -146,7 +148,7 @@ def save_local_articles(articles: Articles) -> Articles:
|
|
|
146
148
|
for alternate in article.alternate
|
|
147
149
|
]
|
|
148
150
|
)
|
|
149
|
-
|
|
151
|
+
commit(LocalArticle)
|
|
150
152
|
return articles
|
|
151
153
|
|
|
152
154
|
|
|
@@ -336,16 +338,93 @@ def get_unread_article_ids() -> list[str]:
|
|
|
336
338
|
##############################################################################
|
|
337
339
|
def clean_old_read_articles(cutoff: timedelta) -> int:
|
|
338
340
|
"""Clean up articles that are older than the given cutoff time."""
|
|
339
|
-
assert LocalArticle._db is not None
|
|
340
341
|
read = get_local_read_article_ids()
|
|
341
342
|
retire_time = datetime.now() - cutoff
|
|
343
|
+
Log().debug(f"Cleaning up read articles published before {retire_time}")
|
|
342
344
|
cleaned = len(
|
|
343
345
|
LocalArticle.where(
|
|
344
346
|
(LocalArticle.published < retire_time) & LocalArticle.id.belongs(read)
|
|
345
347
|
).delete()
|
|
346
348
|
)
|
|
347
|
-
|
|
349
|
+
Log().debug(f"Cleaned: {cleaned}")
|
|
350
|
+
commit(LocalArticle)
|
|
348
351
|
return cleaned
|
|
349
352
|
|
|
350
353
|
|
|
354
|
+
##############################################################################
|
|
355
|
+
def rename_folder_for_articles(rename_from: str | Folder, rename_to: str) -> None:
|
|
356
|
+
"""Rename a folder for all articles that are in that folder.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
rename_from: The folder name to rename from.
|
|
360
|
+
rename_to: The folder name to rename to.
|
|
361
|
+
"""
|
|
362
|
+
rename_from = Folders.full_id(rename_from)
|
|
363
|
+
rename_to = Folders.full_id(rename_to)
|
|
364
|
+
Log().debug(f"Renaming folder for local articles from {rename_from} to {rename_to}")
|
|
365
|
+
LocalArticleCategory.where(LocalArticleCategory.category == rename_from).update(
|
|
366
|
+
category=rename_to
|
|
367
|
+
)
|
|
368
|
+
commit(LocalArticleCategory)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
##############################################################################
|
|
372
|
+
def remove_folder_from_articles(folder: str | Folder) -> None:
|
|
373
|
+
"""Remove a folder from being associated with all articles.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
folder: The folder to remove from all articles.
|
|
377
|
+
"""
|
|
378
|
+
folder = Folders.full_id(folder)
|
|
379
|
+
Log().debug(f"Removing folder {folder} from all local articles")
|
|
380
|
+
LocalArticleCategory.where(LocalArticleCategory.category == folder).delete()
|
|
381
|
+
commit(LocalArticleCategory)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
##############################################################################
|
|
385
|
+
def move_subscription_articles(
|
|
386
|
+
subscription: Subscription,
|
|
387
|
+
from_folder: str | Folder | None,
|
|
388
|
+
to_folder: str | Folder | None,
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Move the articles of a subscription from one folder to another.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
subscription: The subscription whose articles we should move.
|
|
394
|
+
from_folder: The folder to move from.
|
|
395
|
+
to_folder: The folder to move to.
|
|
396
|
+
"""
|
|
397
|
+
from_folder = (
|
|
398
|
+
Folders.full_id(from_folder) if from_folder is not None else from_folder
|
|
399
|
+
)
|
|
400
|
+
to_folder = Folders.full_id(to_folder) if to_folder is not None else to_folder
|
|
401
|
+
Log().debug(
|
|
402
|
+
f"Moving all articles of {subscription.title} ({subscription.id}) from folder {from_folder} to {to_folder}"
|
|
403
|
+
)
|
|
404
|
+
for article in LocalArticle.where(origin_stream_id=subscription.id).join().select():
|
|
405
|
+
if from_folder:
|
|
406
|
+
LocalArticleCategory.where(
|
|
407
|
+
(LocalArticleCategory.article == article.id)
|
|
408
|
+
& (LocalArticleCategory.category == from_folder)
|
|
409
|
+
).delete()
|
|
410
|
+
commit(LocalArticleCategory)
|
|
411
|
+
if to_folder:
|
|
412
|
+
LocalArticleCategory.insert(article=article.id, category=to_folder)
|
|
413
|
+
commit(LocalArticleCategory)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
##############################################################################
|
|
417
|
+
def remove_subscription_articles(subscription: str | Subscription) -> None:
|
|
418
|
+
"""Remove all the articles associated with the given subscription.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
subscription: The subscription to remove the articles for.
|
|
422
|
+
"""
|
|
423
|
+
if isinstance(subscription, Subscription):
|
|
424
|
+
subscription = subscription.id
|
|
425
|
+
Log().debug(f"Removing all local articles for subscription {subscription}")
|
|
426
|
+
LocalArticle.where(origin_stream_id=subscription).delete()
|
|
427
|
+
commit(LocalArticle)
|
|
428
|
+
|
|
429
|
+
|
|
351
430
|
### local_articles.py ends here
|
|
@@ -8,6 +8,10 @@ from oldas import Folder, Folders
|
|
|
8
8
|
# TypeDAL imports.
|
|
9
9
|
from typedal import TypedTable
|
|
10
10
|
|
|
11
|
+
##############################################################################
|
|
12
|
+
# Local imports.
|
|
13
|
+
from .tools import commit
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
##############################################################################
|
|
13
17
|
class LocalFolder(TypedTable):
|
|
@@ -42,12 +46,11 @@ def save_local_folders(folders: Folders) -> Folders:
|
|
|
42
46
|
Returns:
|
|
43
47
|
The folders.
|
|
44
48
|
"""
|
|
45
|
-
assert LocalFolder._db is not None
|
|
46
49
|
LocalFolder.truncate()
|
|
47
50
|
LocalFolder.bulk_insert(
|
|
48
51
|
[{"folder_id": folder.id, "sort_id": folder.sort_id} for folder in folders]
|
|
49
52
|
)
|
|
50
|
-
|
|
53
|
+
commit(LocalFolder)
|
|
51
54
|
return folders
|
|
52
55
|
|
|
53
56
|
|
|
@@ -13,6 +13,10 @@ from oldas.subscriptions import Categories, Category
|
|
|
13
13
|
# TypeDAL imports.
|
|
14
14
|
from typedal import TypedTable
|
|
15
15
|
|
|
16
|
+
##############################################################################
|
|
17
|
+
# Local imports.
|
|
18
|
+
from .tools import commit
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
##############################################################################
|
|
18
22
|
class LocalSubscription(TypedTable):
|
|
@@ -83,29 +87,33 @@ def save_local_subscriptions(subscriptions: Subscriptions) -> Subscriptions:
|
|
|
83
87
|
Returns:
|
|
84
88
|
The subscriptions.
|
|
85
89
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
90
|
+
LocalSubscription.truncate()
|
|
91
|
+
LocalSubscriptionCategory.truncate()
|
|
92
|
+
LocalSubscription.bulk_insert(
|
|
93
|
+
[
|
|
94
|
+
{
|
|
95
|
+
"subscription_id": subscription.id,
|
|
96
|
+
"title": subscription.title,
|
|
97
|
+
"sort_id": subscription.sort_id,
|
|
98
|
+
"first_item_time": subscription.first_item_time,
|
|
99
|
+
"url": subscription.url,
|
|
100
|
+
"html_url": subscription.html_url,
|
|
101
|
+
}
|
|
102
|
+
for subscription in subscriptions
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
LocalSubscriptionCategory.bulk_insert(
|
|
106
|
+
[
|
|
107
|
+
{
|
|
108
|
+
"subscription": subscription.id,
|
|
109
|
+
"category_id": category.id,
|
|
110
|
+
"label": category.label,
|
|
111
|
+
}
|
|
112
|
+
for subscription in subscriptions
|
|
113
|
+
for category in subscription.categories
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
commit(LocalSubscription)
|
|
109
117
|
return subscriptions
|
|
110
118
|
|
|
111
119
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# OldAS imports.
|
|
5
|
-
from oldas import Folders, Subscriptions,
|
|
5
|
+
from oldas import Folders, Subscriptions, id_is_a_feed
|
|
6
6
|
|
|
7
7
|
##############################################################################
|
|
8
8
|
# Local imports.
|
|
@@ -41,7 +41,7 @@ def total_unread(unread: LocalUnread) -> int:
|
|
|
41
41
|
Returns:
|
|
42
42
|
The total unread.
|
|
43
43
|
"""
|
|
44
|
-
return sum(count for category, count in unread.items() if
|
|
44
|
+
return sum(count for category, count in unread.items() if id_is_a_feed(category))
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
### local_unread.py ends here
|