oldnews 0.0.1__tar.gz → 0.0.2__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.0.2/PKG-INFO +86 -0
- oldnews-0.0.2/README.md +54 -0
- oldnews-0.0.2/pyproject.toml +74 -0
- oldnews-0.0.2/src/oldnews/__init__.py +17 -0
- oldnews-0.0.2/src/oldnews/__main__.py +108 -0
- oldnews-0.0.2/src/oldnews/commands/__init__.py +25 -0
- oldnews-0.0.2/src/oldnews/commands/main.py +52 -0
- oldnews-0.0.2/src/oldnews/data/__init__.py +54 -0
- oldnews-0.0.2/src/oldnews/data/auth.py +48 -0
- oldnews-0.0.2/src/oldnews/data/config.py +113 -0
- oldnews-0.0.2/src/oldnews/data/db.py +118 -0
- oldnews-0.0.2/src/oldnews/data/last_grab.py +48 -0
- oldnews-0.0.2/src/oldnews/data/local_articles.py +336 -0
- oldnews-0.0.2/src/oldnews/data/local_folders.py +54 -0
- oldnews-0.0.2/src/oldnews/data/local_subscriptions.py +112 -0
- oldnews-0.0.2/src/oldnews/data/local_unread.py +47 -0
- oldnews-0.0.2/src/oldnews/data/locations.py +58 -0
- oldnews-0.0.2/src/oldnews/data/navigation_state.py +42 -0
- oldnews-0.0.2/src/oldnews/oldnews.py +110 -0
- oldnews-0.0.2/src/oldnews/providers/__init__.py +13 -0
- oldnews-0.0.2/src/oldnews/providers/main.py +46 -0
- oldnews-0.0.2/src/oldnews/screens/__init__.py +12 -0
- oldnews-0.0.2/src/oldnews/screens/login.py +103 -0
- oldnews-0.0.2/src/oldnews/screens/main.py +489 -0
- oldnews-0.0.2/src/oldnews/widgets/__init__.py +13 -0
- oldnews-0.0.2/src/oldnews/widgets/article_content.py +91 -0
- oldnews-0.0.2/src/oldnews/widgets/article_list.py +207 -0
- oldnews-0.0.2/src/oldnews/widgets/navigation.py +240 -0
- oldnews-0.0.1/PKG-INFO +0 -9
- oldnews-0.0.1/pyproject.toml +0 -33
- oldnews-0.0.1/src/oldnews/__init__.py +0 -2
- /oldnews-0.0.1/README.md → /oldnews-0.0.2/src/oldnews/py.typed +0 -0
oldnews-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oldnews
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A terminal-based client for TheOldReader
|
|
5
|
+
Keywords: atom,client,Google Reader,RSS,TheOldReader,terminal,news-reader,news
|
|
6
|
+
Author: Dave Pearson
|
|
7
|
+
Author-email: Dave Pearson <davep@davep.org>
|
|
8
|
+
License-Expression: GPL-3.0-or-later
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: html-to-markdown>=2.15.0
|
|
19
|
+
Requires-Dist: oldas>=0.3.1
|
|
20
|
+
Requires-Dist: textual>=6.3.0
|
|
21
|
+
Requires-Dist: textual-enhanced>=1.2.0
|
|
22
|
+
Requires-Dist: typedal>=4.2.2
|
|
23
|
+
Requires-Dist: xdg-base-dirs>=6.0.2
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Project-URL: Discussions, https://github.com/davep/oldnews/discussions
|
|
26
|
+
Project-URL: Documentation, https://oldnews.davep.dev/
|
|
27
|
+
Project-URL: Homepage, https://github.com/davep/oldnews
|
|
28
|
+
Project-URL: Issues, https://github.com/davep/oldnews/issues
|
|
29
|
+
Project-URL: Repository, https://github.com/davep/oldnews
|
|
30
|
+
Project-URL: Source, https://github.com/davep/oldnews
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# OldNews - A TheOldReader client for the terminal
|
|
34
|
+
|
|
35
|
+
## Introduction
|
|
36
|
+
|
|
37
|
+
OldNews is a terminal-based client for
|
|
38
|
+
[TheOldReader](https://theoldreader.com). Right now it is an evolving work
|
|
39
|
+
in progress, built on top of [`oldas`](https://github.com/davep/oldas).
|
|
40
|
+
|
|
41
|
+
## Installing
|
|
42
|
+
|
|
43
|
+
### pipx
|
|
44
|
+
|
|
45
|
+
The application can be installed using [`pipx`](https://pypa.github.io/pipx/):
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
$ pipx install oldnews
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### uv
|
|
52
|
+
|
|
53
|
+
The package can be install using [`uv`](https://docs.astral.sh/uv/getting-started/installation/):
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
uv tool install oldnews
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## File locations
|
|
60
|
+
|
|
61
|
+
OldNews stores files in a `oldnews` directory within both [`$XDG_DATA_HOME` and
|
|
62
|
+
`$XDG_CONFIG_HOME`](https://specifications.freedesktop.org/basedir-spec/latest/).
|
|
63
|
+
If you wish to fully remove anything to do with OldNews you will need to
|
|
64
|
+
remove those directories too.
|
|
65
|
+
|
|
66
|
+
Expanding for the common locations, the files normally created are:
|
|
67
|
+
|
|
68
|
+
- `~/.config/oldnews/configuration.json` -- The configuration file.
|
|
69
|
+
- `~/.local/share/oldnews/*` -- The locally-held data.
|
|
70
|
+
|
|
71
|
+
## Getting help
|
|
72
|
+
|
|
73
|
+
If you need help, or have any ideas, please feel free to [raise an
|
|
74
|
+
issue](https://github.com/davep/oldnews/issues) or [start a
|
|
75
|
+
discussion](https://github.com/davep/oldnews/discussions). However, please
|
|
76
|
+
keep in mind that at the moment the application is very much an ongoing work
|
|
77
|
+
in progress; expect lots of obvious functionality to be missing and "coming
|
|
78
|
+
soon"; perhaps also expect bugs.
|
|
79
|
+
|
|
80
|
+
## TODO
|
|
81
|
+
|
|
82
|
+
See [the TODO tag in
|
|
83
|
+
issues](https://github.com/davep/oldnews/issues?q=is%3Aissue+is%3Aopen+label%3ATODO)
|
|
84
|
+
to see what I'm planning.
|
|
85
|
+
|
|
86
|
+
[//]: # (README.md ends here)
|
oldnews-0.0.2/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# OldNews - A TheOldReader client for the terminal
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
OldNews is a terminal-based client for
|
|
6
|
+
[TheOldReader](https://theoldreader.com). Right now it is an evolving work
|
|
7
|
+
in progress, built on top of [`oldas`](https://github.com/davep/oldas).
|
|
8
|
+
|
|
9
|
+
## Installing
|
|
10
|
+
|
|
11
|
+
### pipx
|
|
12
|
+
|
|
13
|
+
The application can be installed using [`pipx`](https://pypa.github.io/pipx/):
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
$ pipx install oldnews
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### uv
|
|
20
|
+
|
|
21
|
+
The package can be install using [`uv`](https://docs.astral.sh/uv/getting-started/installation/):
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv tool install oldnews
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## File locations
|
|
28
|
+
|
|
29
|
+
OldNews stores files in a `oldnews` directory within both [`$XDG_DATA_HOME` and
|
|
30
|
+
`$XDG_CONFIG_HOME`](https://specifications.freedesktop.org/basedir-spec/latest/).
|
|
31
|
+
If you wish to fully remove anything to do with OldNews you will need to
|
|
32
|
+
remove those directories too.
|
|
33
|
+
|
|
34
|
+
Expanding for the common locations, the files normally created are:
|
|
35
|
+
|
|
36
|
+
- `~/.config/oldnews/configuration.json` -- The configuration file.
|
|
37
|
+
- `~/.local/share/oldnews/*` -- The locally-held data.
|
|
38
|
+
|
|
39
|
+
## Getting help
|
|
40
|
+
|
|
41
|
+
If you need help, or have any ideas, please feel free to [raise an
|
|
42
|
+
issue](https://github.com/davep/oldnews/issues) or [start a
|
|
43
|
+
discussion](https://github.com/davep/oldnews/discussions). However, please
|
|
44
|
+
keep in mind that at the moment the application is very much an ongoing work
|
|
45
|
+
in progress; expect lots of obvious functionality to be missing and "coming
|
|
46
|
+
soon"; perhaps also expect bugs.
|
|
47
|
+
|
|
48
|
+
## TODO
|
|
49
|
+
|
|
50
|
+
See [the TODO tag in
|
|
51
|
+
issues](https://github.com/davep/oldnews/issues?q=is%3Aissue+is%3Aopen+label%3ATODO)
|
|
52
|
+
to see what I'm planning.
|
|
53
|
+
|
|
54
|
+
[//]: # (README.md ends here)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oldnews"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "A terminal-based client for TheOldReader"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Dave Pearson", email = "davep@davep.org" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "GPL-3.0-or-later"
|
|
11
|
+
keywords = [
|
|
12
|
+
"atom",
|
|
13
|
+
"client",
|
|
14
|
+
"Google Reader",
|
|
15
|
+
"RSS",
|
|
16
|
+
"TheOldReader",
|
|
17
|
+
"terminal",
|
|
18
|
+
"news-reader",
|
|
19
|
+
"news",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"html-to-markdown>=2.15.0",
|
|
23
|
+
"oldas>=0.3.1",
|
|
24
|
+
"textual>=6.3.0",
|
|
25
|
+
"textual-enhanced>=1.2.0",
|
|
26
|
+
"typedal>=4.2.2",
|
|
27
|
+
"xdg-base-dirs>=6.0.2",
|
|
28
|
+
]
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Development Status :: 3 - Alpha",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Programming Language :: Python :: 3.11",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Programming Language :: Python :: 3.13",
|
|
37
|
+
"Programming Language :: Python :: 3.14",
|
|
38
|
+
"Typing :: Typed",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/davep/oldnews"
|
|
43
|
+
Repository = "https://github.com/davep/oldnews"
|
|
44
|
+
Documentation = "https://oldnews.davep.dev/"
|
|
45
|
+
Source = "https://github.com/davep/oldnews"
|
|
46
|
+
Issues = "https://github.com/davep/oldnews/issues"
|
|
47
|
+
Discussions = "https://github.com/davep/oldnews/discussions"
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
oldnews = "oldnews.__main__:main"
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["uv_build>=0.9.4,<0.10.0"]
|
|
54
|
+
build-backend = "uv_build"
|
|
55
|
+
|
|
56
|
+
[[tool.uv.index]]
|
|
57
|
+
name = "testpypi"
|
|
58
|
+
url = "https://test.pypi.org/simple/"
|
|
59
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
60
|
+
explicit = true
|
|
61
|
+
|
|
62
|
+
[tool.pyright]
|
|
63
|
+
venvPath="."
|
|
64
|
+
venv=".venv"
|
|
65
|
+
exclude=[".venv"]
|
|
66
|
+
|
|
67
|
+
[dependency-groups]
|
|
68
|
+
dev = [
|
|
69
|
+
"codespell>=2.4.1",
|
|
70
|
+
"mypy>=1.18.2",
|
|
71
|
+
"pre-commit>=4.3.0",
|
|
72
|
+
"ruff>=0.14.0",
|
|
73
|
+
"textual-dev>=1.8.0",
|
|
74
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""A terminal-based client for TheOldReader."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Python imports.
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
|
|
7
|
+
##############################################################################
|
|
8
|
+
# Main app information.
|
|
9
|
+
__author__ = "Dave Pearson"
|
|
10
|
+
__copyright__ = "Copyright 2025, Dave Pearson"
|
|
11
|
+
__credits__ = ["Dave Pearson"]
|
|
12
|
+
__maintainer__ = "Dave Pearson"
|
|
13
|
+
__email__ = "davep@davep.org"
|
|
14
|
+
__version__ = version("oldnews")
|
|
15
|
+
__licence__ = "GPLv3+"
|
|
16
|
+
|
|
17
|
+
### __init__.py ends here
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""The main entry point to the application."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Python imports.
|
|
5
|
+
from argparse import ArgumentParser, Namespace
|
|
6
|
+
from inspect import cleandoc
|
|
7
|
+
from operator import attrgetter
|
|
8
|
+
|
|
9
|
+
##############################################################################
|
|
10
|
+
# Local imports.
|
|
11
|
+
from . import __doc__, __version__
|
|
12
|
+
from .data import initialise_database
|
|
13
|
+
from .oldnews import OldNews
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
##############################################################################
|
|
17
|
+
def get_args() -> Namespace:
|
|
18
|
+
"""Get the command line arguments.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The arguments.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Build the parser.
|
|
25
|
+
parser = ArgumentParser(
|
|
26
|
+
prog="oldnews",
|
|
27
|
+
description=__doc__,
|
|
28
|
+
epilog=f"v{__version__}",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Add --version
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-v",
|
|
34
|
+
"--version",
|
|
35
|
+
help="Show version information",
|
|
36
|
+
action="version",
|
|
37
|
+
version=f"%(prog)s v{__version__}",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Add --license
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--license",
|
|
43
|
+
"--licence",
|
|
44
|
+
help="Show license information",
|
|
45
|
+
action="store_true",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Add --bindings
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-b",
|
|
51
|
+
"--bindings",
|
|
52
|
+
help="List commands that can have their bindings changed",
|
|
53
|
+
action="store_true",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Add --theme
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"-t",
|
|
59
|
+
"--theme",
|
|
60
|
+
help="Set the theme for the application (set to ? to list available themes)",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Finally, parse the command line.
|
|
64
|
+
return parser.parse_args()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
##############################################################################
|
|
68
|
+
def show_bindable_commands() -> None:
|
|
69
|
+
"""Show the commands that can have bindings applied."""
|
|
70
|
+
from rich.console import Console
|
|
71
|
+
from rich.markup import escape
|
|
72
|
+
|
|
73
|
+
from .screens import Main
|
|
74
|
+
|
|
75
|
+
console = Console(highlight=False)
|
|
76
|
+
for command in sorted(Main.COMMAND_MESSAGES, key=attrgetter("__name__")):
|
|
77
|
+
if command().has_binding:
|
|
78
|
+
console.print(
|
|
79
|
+
f"[bold]{escape(command.__name__)}[/] [dim italic]- {escape(command.tooltip())}[/]"
|
|
80
|
+
)
|
|
81
|
+
console.print(
|
|
82
|
+
f" [dim italic]Default: {escape(command.binding().key)}[/]"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
##############################################################################
|
|
87
|
+
def show_themes() -> None:
|
|
88
|
+
"""Show the available themes."""
|
|
89
|
+
for theme in sorted(OldNews(Namespace(theme=None)).available_themes):
|
|
90
|
+
if theme != "textual-ansi":
|
|
91
|
+
print(theme)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
##############################################################################
|
|
95
|
+
def main() -> None:
|
|
96
|
+
"""Main entry function."""
|
|
97
|
+
if (args := get_args()).license:
|
|
98
|
+
print(cleandoc(OldNews.HELP_LICENSE))
|
|
99
|
+
elif args.bindings:
|
|
100
|
+
show_bindable_commands()
|
|
101
|
+
elif args.theme == "?":
|
|
102
|
+
show_themes()
|
|
103
|
+
else:
|
|
104
|
+
initialise_database()
|
|
105
|
+
OldNews(args).run()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
### __main__.py ends here
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Provides the command messages for the application."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Local imports.
|
|
5
|
+
from .main import (
|
|
6
|
+
Escape,
|
|
7
|
+
NextUnread,
|
|
8
|
+
OpenArticle,
|
|
9
|
+
PreviousUnread,
|
|
10
|
+
RefreshFromTheOldReader,
|
|
11
|
+
ToggleShowAll,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
##############################################################################
|
|
15
|
+
# Exports.
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Escape",
|
|
18
|
+
"NextUnread",
|
|
19
|
+
"OpenArticle",
|
|
20
|
+
"PreviousUnread",
|
|
21
|
+
"RefreshFromTheOldReader",
|
|
22
|
+
"ToggleShowAll",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
### __init__.py ends here
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The main commands used within the application."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Textual enhanced imports.
|
|
5
|
+
from textual_enhanced.commands import Command
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
##############################################################################
|
|
9
|
+
class RefreshFromTheOldReader(Command):
|
|
10
|
+
"""Connect to TheOldReader and refresh the local articles"""
|
|
11
|
+
|
|
12
|
+
BINDING_KEY = "ctrl+r"
|
|
13
|
+
SHOW_IN_FOOTER = True
|
|
14
|
+
FOOTER_TEXT = "Refresh"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
##############################################################################
|
|
18
|
+
class ToggleShowAll(Command):
|
|
19
|
+
"""Toggle between showing all and showing only unread"""
|
|
20
|
+
|
|
21
|
+
BINDING_KEY = "f2"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
##############################################################################
|
|
25
|
+
class Escape(Command):
|
|
26
|
+
"""Back out through the panes, or exit the app if the navigation pane has focus"""
|
|
27
|
+
|
|
28
|
+
BINDING_KEY = "escape, q"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
##############################################################################
|
|
32
|
+
class NextUnread(Command):
|
|
33
|
+
"""Navigate to the next unread article in the currently-selected category"""
|
|
34
|
+
|
|
35
|
+
BINDING_KEY = "n"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
##############################################################################
|
|
39
|
+
class PreviousUnread(Command):
|
|
40
|
+
"""Navigate to the previous unread article in the currently-selected category"""
|
|
41
|
+
|
|
42
|
+
BINDING_KEY = "p"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
##############################################################################
|
|
46
|
+
class OpenArticle(Command):
|
|
47
|
+
"""Open the current article in the web browser"""
|
|
48
|
+
|
|
49
|
+
BINDING_KEY = "o"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### main.py ends here
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Provides functions and classes for managing the app's data."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Local imports.
|
|
5
|
+
from .auth import get_auth_token, set_auth_token
|
|
6
|
+
from .config import (
|
|
7
|
+
Configuration,
|
|
8
|
+
load_configuration,
|
|
9
|
+
save_configuration,
|
|
10
|
+
update_configuration,
|
|
11
|
+
)
|
|
12
|
+
from .db import initialise_database
|
|
13
|
+
from .last_grab import last_grabbed_data_at, remember_we_last_grabbed_at
|
|
14
|
+
from .local_articles import (
|
|
15
|
+
get_local_articles,
|
|
16
|
+
get_unread_article_ids,
|
|
17
|
+
locally_mark_article_ids_read,
|
|
18
|
+
locally_mark_read,
|
|
19
|
+
save_local_articles,
|
|
20
|
+
)
|
|
21
|
+
from .local_folders import get_local_folders, save_local_folders
|
|
22
|
+
from .local_subscriptions import get_local_subscriptions, save_local_subscriptions
|
|
23
|
+
from .local_unread import LocalUnread, get_local_unread, total_unread
|
|
24
|
+
from .navigation_state import get_navigation_state, save_navigation_state
|
|
25
|
+
|
|
26
|
+
##############################################################################
|
|
27
|
+
# Exports.
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Configuration",
|
|
30
|
+
"get_auth_token",
|
|
31
|
+
"get_local_articles",
|
|
32
|
+
"get_local_folders",
|
|
33
|
+
"get_local_subscriptions",
|
|
34
|
+
"get_local_unread",
|
|
35
|
+
"get_navigation_state",
|
|
36
|
+
"get_unread_article_ids",
|
|
37
|
+
"initialise_database",
|
|
38
|
+
"last_grabbed_data_at",
|
|
39
|
+
"load_configuration",
|
|
40
|
+
"locally_mark_read",
|
|
41
|
+
"locally_mark_article_ids_read",
|
|
42
|
+
"LocalUnread",
|
|
43
|
+
"remember_we_last_grabbed_at",
|
|
44
|
+
"save_configuration",
|
|
45
|
+
"save_local_articles",
|
|
46
|
+
"save_local_folders",
|
|
47
|
+
"save_local_subscriptions",
|
|
48
|
+
"save_navigation_state",
|
|
49
|
+
"set_auth_token",
|
|
50
|
+
"total_unread",
|
|
51
|
+
"update_configuration",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
### __init__.py ends here
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Code relating to TOR auth."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Python imports.
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
##############################################################################
|
|
8
|
+
# Local imports.
|
|
9
|
+
from .locations import data_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
##############################################################################
|
|
13
|
+
def auth_token_file() -> Path:
|
|
14
|
+
"""The location of the token file.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The path to the token file.
|
|
18
|
+
"""
|
|
19
|
+
return data_dir() / ".token"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
##############################################################################
|
|
23
|
+
def get_auth_token() -> str | None:
|
|
24
|
+
"""Get the TOR auth token.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The token, if there is one, otherwise `None`.
|
|
28
|
+
"""
|
|
29
|
+
if not auth_token_file().is_file():
|
|
30
|
+
return None
|
|
31
|
+
return auth_token_file().read_text(encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
##############################################################################
|
|
35
|
+
def set_auth_token(token: str) -> str:
|
|
36
|
+
"""Set the TOR auth token for later use.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
token: The token to use.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The token.
|
|
43
|
+
"""
|
|
44
|
+
auth_token_file().write_text(token, encoding="utf-8")
|
|
45
|
+
return token
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### auth.py ends here
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Code relating to the application's configuration file."""
|
|
2
|
+
|
|
3
|
+
##############################################################################
|
|
4
|
+
# Python imports.
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from json import dumps, loads
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterator
|
|
11
|
+
|
|
12
|
+
##############################################################################
|
|
13
|
+
# Local imports.
|
|
14
|
+
from .locations import config_dir
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
##############################################################################
|
|
18
|
+
@dataclass
|
|
19
|
+
class Configuration:
|
|
20
|
+
"""The configuration data for the application."""
|
|
21
|
+
|
|
22
|
+
theme: str | None = None
|
|
23
|
+
"""The theme for the application."""
|
|
24
|
+
|
|
25
|
+
bindings: dict[str, str] = field(default_factory=dict)
|
|
26
|
+
"""Command keyboard binding overrides."""
|
|
27
|
+
|
|
28
|
+
local_history: int = 28
|
|
29
|
+
"""The number of days to keep a local copy of an article."""
|
|
30
|
+
|
|
31
|
+
show_all: bool = False
|
|
32
|
+
"""Should we show all articles, or just unread articles?"""
|
|
33
|
+
|
|
34
|
+
mark_read_on_read_timeout: float = 0.25
|
|
35
|
+
"""How long to wait before marking an article as read, upon reading it."""
|
|
36
|
+
|
|
37
|
+
startup_refresh_holdoff_period: float = 600
|
|
38
|
+
"""The number of seconds to wait before hitting TheOldReader again on startup."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
##############################################################################
|
|
42
|
+
def configuration_file() -> Path:
|
|
43
|
+
"""The path to the file that holds the application configuration.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The path to the configuration file.
|
|
47
|
+
"""
|
|
48
|
+
return config_dir() / "configuration.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
##############################################################################
|
|
52
|
+
def save_configuration(configuration: Configuration) -> Configuration:
|
|
53
|
+
"""Save the given configuration.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
The configuration to store.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The configuration.
|
|
60
|
+
"""
|
|
61
|
+
load_configuration.cache_clear()
|
|
62
|
+
configuration_file().write_text(
|
|
63
|
+
dumps(asdict(configuration), indent=4), encoding="utf-8"
|
|
64
|
+
)
|
|
65
|
+
return load_configuration()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
##############################################################################
|
|
69
|
+
@lru_cache(maxsize=None)
|
|
70
|
+
def load_configuration() -> Configuration:
|
|
71
|
+
"""Load the configuration.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The configuration.
|
|
75
|
+
|
|
76
|
+
Note:
|
|
77
|
+
As a side-effect, if the configuration doesn't exist a default one
|
|
78
|
+
will be saved to storage.
|
|
79
|
+
|
|
80
|
+
This function is designed so that it's safe and low-cost to
|
|
81
|
+
repeatedly call it. The configuration is cached and will only be
|
|
82
|
+
loaded from storage when necessary.
|
|
83
|
+
"""
|
|
84
|
+
source = configuration_file()
|
|
85
|
+
return (
|
|
86
|
+
Configuration(**loads(source.read_text(encoding="utf-8")))
|
|
87
|
+
if source.exists()
|
|
88
|
+
else save_configuration(Configuration())
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
##############################################################################
|
|
93
|
+
@contextmanager
|
|
94
|
+
def update_configuration() -> Iterator[Configuration]:
|
|
95
|
+
"""Context manager for updating the configuration.
|
|
96
|
+
|
|
97
|
+
Loads the configuration and makes it available, then ensures it is
|
|
98
|
+
saved.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
```python
|
|
102
|
+
with update_configuration() as config:
|
|
103
|
+
config.meaning = 42
|
|
104
|
+
```
|
|
105
|
+
"""
|
|
106
|
+
configuration = load_configuration()
|
|
107
|
+
try:
|
|
108
|
+
yield configuration
|
|
109
|
+
finally:
|
|
110
|
+
save_configuration(configuration)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
### config.py ends here
|