oldnews 0.0.1__tar.gz → 0.1.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.
Files changed (32) hide show
  1. oldnews-0.1.0/PKG-INFO +86 -0
  2. oldnews-0.1.0/README.md +54 -0
  3. oldnews-0.1.0/pyproject.toml +74 -0
  4. oldnews-0.1.0/src/oldnews/__init__.py +17 -0
  5. oldnews-0.1.0/src/oldnews/__main__.py +108 -0
  6. oldnews-0.1.0/src/oldnews/commands/__init__.py +31 -0
  7. oldnews-0.1.0/src/oldnews/commands/main.py +73 -0
  8. oldnews-0.1.0/src/oldnews/data/__init__.py +54 -0
  9. oldnews-0.1.0/src/oldnews/data/auth.py +48 -0
  10. oldnews-0.1.0/src/oldnews/data/config.py +113 -0
  11. oldnews-0.1.0/src/oldnews/data/db.py +118 -0
  12. oldnews-0.1.0/src/oldnews/data/last_grab.py +48 -0
  13. oldnews-0.1.0/src/oldnews/data/local_articles.py +336 -0
  14. oldnews-0.1.0/src/oldnews/data/local_folders.py +54 -0
  15. oldnews-0.1.0/src/oldnews/data/local_subscriptions.py +112 -0
  16. oldnews-0.1.0/src/oldnews/data/local_unread.py +47 -0
  17. oldnews-0.1.0/src/oldnews/data/locations.py +58 -0
  18. oldnews-0.1.0/src/oldnews/data/navigation_state.py +42 -0
  19. oldnews-0.1.0/src/oldnews/oldnews.py +110 -0
  20. oldnews-0.1.0/src/oldnews/providers/__init__.py +13 -0
  21. oldnews-0.1.0/src/oldnews/providers/main.py +52 -0
  22. oldnews-0.1.0/src/oldnews/screens/__init__.py +12 -0
  23. oldnews-0.1.0/src/oldnews/screens/login.py +103 -0
  24. oldnews-0.1.0/src/oldnews/screens/main.py +550 -0
  25. oldnews-0.1.0/src/oldnews/widgets/__init__.py +13 -0
  26. oldnews-0.1.0/src/oldnews/widgets/article_content.py +91 -0
  27. oldnews-0.1.0/src/oldnews/widgets/article_list.py +225 -0
  28. oldnews-0.1.0/src/oldnews/widgets/navigation.py +240 -0
  29. oldnews-0.0.1/PKG-INFO +0 -9
  30. oldnews-0.0.1/pyproject.toml +0 -33
  31. oldnews-0.0.1/src/oldnews/__init__.py +0 -2
  32. /oldnews-0.0.1/README.md → /oldnews-0.1.0/src/oldnews/py.typed +0 -0
oldnews-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: oldnews
3
+ Version: 0.1.0
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: Homepage, https://github.com/davep/oldnews
26
+ Project-URL: Repository, https://github.com/davep/oldnews
27
+ Project-URL: Documentation, https://oldnews.davep.dev/
28
+ Project-URL: Source, https://github.com/davep/oldnews
29
+ Project-URL: Issues, https://github.com/davep/oldnews/issues
30
+ Project-URL: Discussions, https://github.com/davep/oldnews/discussions
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)
@@ -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.1.0"
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,31 @@
1
+ """Provides the command messages for the application."""
2
+
3
+ ##############################################################################
4
+ # Local imports.
5
+ from .main import (
6
+ Escape,
7
+ MarkAllRead,
8
+ Next,
9
+ NextUnread,
10
+ OpenArticle,
11
+ Previous,
12
+ PreviousUnread,
13
+ RefreshFromTheOldReader,
14
+ ToggleShowAll,
15
+ )
16
+
17
+ ##############################################################################
18
+ # Exports.
19
+ __all__ = [
20
+ "Escape",
21
+ "MarkAllRead",
22
+ "Next",
23
+ "NextUnread",
24
+ "OpenArticle",
25
+ "Previous",
26
+ "PreviousUnread",
27
+ "RefreshFromTheOldReader",
28
+ "ToggleShowAll",
29
+ ]
30
+
31
+ ### __init__.py ends here
@@ -0,0 +1,73 @@
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 Next(Command):
40
+ """Navigate to the next article regardless of read status"""
41
+
42
+ BINDING_KEY = "N"
43
+
44
+
45
+ ##############################################################################
46
+ class PreviousUnread(Command):
47
+ """Navigate to the previous unread article in the currently-selected category"""
48
+
49
+ BINDING_KEY = "p"
50
+
51
+
52
+ ##############################################################################
53
+ class Previous(Command):
54
+ """Navigate to the next article regardless of read status"""
55
+
56
+ BINDING_KEY = "P"
57
+
58
+
59
+ ##############################################################################
60
+ class OpenArticle(Command):
61
+ """Open the current article in the web browser"""
62
+
63
+ BINDING_KEY = "o"
64
+
65
+
66
+ ##############################################################################
67
+ class MarkAllRead(Command):
68
+ """Mark all unread articles in the current category as read"""
69
+
70
+ BINDING_KEY = "R"
71
+
72
+
73
+ ### 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