bejw 0.2.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.
- bejw-0.2.0/PKG-INFO +39 -0
- bejw-0.2.0/README.md +25 -0
- bejw-0.2.0/bejw/__init__.py +0 -0
- bejw-0.2.0/bejw/main.py +88 -0
- bejw-0.2.0/bejw/models.py +56 -0
- bejw-0.2.0/bejw/render.py +24 -0
- bejw-0.2.0/bejw/storage.py +22 -0
- bejw-0.2.0/bejw.egg-info/PKG-INFO +39 -0
- bejw-0.2.0/bejw.egg-info/SOURCES.txt +15 -0
- bejw-0.2.0/bejw.egg-info/dependency_links.txt +1 -0
- bejw-0.2.0/bejw.egg-info/entry_points.txt +2 -0
- bejw-0.2.0/bejw.egg-info/requires.txt +2 -0
- bejw-0.2.0/bejw.egg-info/top_level.txt +1 -0
- bejw-0.2.0/pyproject.toml +29 -0
- bejw-0.2.0/setup.cfg +4 -0
- bejw-0.2.0/tests/test_models.py +52 -0
- bejw-0.2.0/tests/test_storage.py +27 -0
bejw-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bejw
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A capped reading list for links that shimmer
|
|
5
|
+
Author: menisadi
|
|
6
|
+
Project-URL: Homepage, https://github.com/menisadi/bejw
|
|
7
|
+
Project-URL: Repository, https://github.com/menisadi/bejw
|
|
8
|
+
Project-URL: Changelog, https://github.com/menisadi/bejw/blob/main/CHANGELOG.md
|
|
9
|
+
Keywords: cli,reading-list,links,productivity
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: rich>=14.3.2
|
|
13
|
+
Requires-Dist: typer>=0.21.1
|
|
14
|
+
|
|
15
|
+
# bejw
|
|
16
|
+
|
|
17
|
+
A small CLI to keep a capped reading list of links in a local JSON file.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv sync
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python main.py --help
|
|
29
|
+
python main.py init --capacity 10
|
|
30
|
+
python main.py add "https://example.com" "Example"
|
|
31
|
+
python main.py list
|
|
32
|
+
python main.py remove <id>
|
|
33
|
+
python main.py capacity 5
|
|
34
|
+
python main.py clear
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Storage
|
|
38
|
+
|
|
39
|
+
By default, data is stored in `~/.bejw/links.json`.
|
bejw-0.2.0/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# bejw
|
|
2
|
+
|
|
3
|
+
A small CLI to keep a capped reading list of links in a local JSON file.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv sync
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python main.py --help
|
|
15
|
+
python main.py init --capacity 10
|
|
16
|
+
python main.py add "https://example.com" "Example"
|
|
17
|
+
python main.py list
|
|
18
|
+
python main.py remove <id>
|
|
19
|
+
python main.py capacity 5
|
|
20
|
+
python main.py clear
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Storage
|
|
24
|
+
|
|
25
|
+
By default, data is stored in `~/.bejw/links.json`.
|
|
File without changes
|
bejw-0.2.0/bejw/main.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bejw: A capped reading list for links that shimmer
|
|
3
|
+
"""
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from .models import CapacityError, ReadingList
|
|
9
|
+
from .render import render_links
|
|
10
|
+
from .storage import load, save
|
|
11
|
+
|
|
12
|
+
DEFAULT_CAPACITY = 10
|
|
13
|
+
DEFAULT_FILE_PATH = "~/.bejw/links.json"
|
|
14
|
+
|
|
15
|
+
app = typer.Typer()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.callback(invoke_without_command=True)
|
|
19
|
+
def main(context: typer.Context) -> None:
|
|
20
|
+
"""Entry point that shows basic info when no subcommand is provided."""
|
|
21
|
+
if context.invoked_subcommand is not None:
|
|
22
|
+
return
|
|
23
|
+
typer.echo("bejw: A capped reading list for links that shimmer")
|
|
24
|
+
typer.echo("Run with --help to see commands and options.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def init(capacity: int = DEFAULT_CAPACITY, file_path: str = DEFAULT_FILE_PATH) -> None:
|
|
29
|
+
"""Initialize the reading list with a specified capacity and file path."""
|
|
30
|
+
# NOTE: this will override existing reading list
|
|
31
|
+
# TODO: add a confirmation prompt before overriding
|
|
32
|
+
reading_list = ReadingList(capacity=capacity)
|
|
33
|
+
save(reading_list, file_path)
|
|
34
|
+
expanded_path = str(Path(file_path).expanduser())
|
|
35
|
+
typer.echo(f"Initialized reading list at {expanded_path} with capacity {capacity}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def add(url: str, title: str, file_path: str = DEFAULT_FILE_PATH) -> None:
|
|
40
|
+
"""Add a link to the reading list."""
|
|
41
|
+
reading_list = load(file_path)
|
|
42
|
+
try:
|
|
43
|
+
link = reading_list.add_link(url, title)
|
|
44
|
+
except CapacityError:
|
|
45
|
+
typer.echo("Reading list is full! Please remove a link before adding a new one.")
|
|
46
|
+
raise typer.Exit(code=1)
|
|
47
|
+
save(reading_list, file_path)
|
|
48
|
+
typer.echo(f"Added {link.id}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
def remove(link_id: str, file_path: str = DEFAULT_FILE_PATH) -> None:
|
|
53
|
+
"""Remove a link from the reading list by id."""
|
|
54
|
+
reading_list = load(file_path)
|
|
55
|
+
removed = reading_list.remove_link(link_id)
|
|
56
|
+
save(reading_list, file_path)
|
|
57
|
+
if not removed:
|
|
58
|
+
typer.echo("No link found with that id.")
|
|
59
|
+
raise typer.Exit(code=1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command()
|
|
63
|
+
def list(file_path: str = DEFAULT_FILE_PATH, show_ids: bool = False) -> None:
|
|
64
|
+
"""Display the reading list."""
|
|
65
|
+
reading_list = load(file_path)
|
|
66
|
+
render_links(reading_list, show_ids=show_ids)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command()
|
|
70
|
+
def capacity(value: int, file_path: str = DEFAULT_FILE_PATH) -> None:
|
|
71
|
+
"""Change the capacity of the reading list."""
|
|
72
|
+
reading_list = load(file_path)
|
|
73
|
+
reading_list.capacity = value
|
|
74
|
+
save(reading_list, file_path)
|
|
75
|
+
typer.echo(f"Capacity set to {value}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command()
|
|
79
|
+
def clear(file_path: str = DEFAULT_FILE_PATH) -> None:
|
|
80
|
+
"""Clear the reading list."""
|
|
81
|
+
reading_list = load(file_path)
|
|
82
|
+
reading_list.clear_links()
|
|
83
|
+
save(reading_list, file_path)
|
|
84
|
+
typer.echo("Reading list cleared")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
app()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CapacityError(Exception):
|
|
9
|
+
"""Raised when trying to add a link to a full reading list."""
|
|
10
|
+
def __init__(self, message: str = "Reading list is full") -> None:
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Link:
|
|
17
|
+
id: str
|
|
18
|
+
url: str
|
|
19
|
+
title: str
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def create(url: str, title: str) -> "Link":
|
|
23
|
+
return Link(id=str(uuid4()), url=url, title=title)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReadingList:
|
|
27
|
+
def __init__(self, capacity: int = 10, links: Iterable[Link] | None = None) -> None:
|
|
28
|
+
self.capacity = capacity
|
|
29
|
+
self.links: list[Link] = list(links or [])
|
|
30
|
+
|
|
31
|
+
def add_link(self, url: str, title: str) -> Link:
|
|
32
|
+
if len(self.links) >= self.capacity:
|
|
33
|
+
raise CapacityError("Reading list is full")
|
|
34
|
+
link = Link.create(url, title)
|
|
35
|
+
self.links.append(link)
|
|
36
|
+
return link
|
|
37
|
+
|
|
38
|
+
def remove_link(self, link_id: str) -> bool:
|
|
39
|
+
before = len(self.links)
|
|
40
|
+
self.links = [link for link in self.links if link.id != link_id]
|
|
41
|
+
return len(self.links) < before
|
|
42
|
+
|
|
43
|
+
def clear_links(self) -> None:
|
|
44
|
+
self.links = []
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"capacity": self.capacity,
|
|
49
|
+
"links": [link.__dict__ for link in self.links],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def from_dict(data: dict) -> "ReadingList":
|
|
54
|
+
links = [Link(**item) for item in data.get("links", [])]
|
|
55
|
+
capacity = data.get("capacity", 10)
|
|
56
|
+
return ReadingList(capacity=capacity, links=links)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
|
|
4
|
+
from .models import ReadingList
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_links(reading_list: ReadingList, show_ids: bool = False) -> None:
|
|
10
|
+
table = Table(title="Bejeweled Reading List")
|
|
11
|
+
if show_ids:
|
|
12
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
13
|
+
table.add_column("Title", style="magenta")
|
|
14
|
+
table.add_column("URL", style="green")
|
|
15
|
+
|
|
16
|
+
for link in reading_list.links:
|
|
17
|
+
row = []
|
|
18
|
+
if show_ids:
|
|
19
|
+
row.append(str(link.id))
|
|
20
|
+
row.append(link.title)
|
|
21
|
+
row.append(link.url)
|
|
22
|
+
table.add_row(*row)
|
|
23
|
+
|
|
24
|
+
console.print(table)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .models import ReadingList
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load(file_path: str) -> ReadingList:
|
|
10
|
+
"""Load the reading list from a JSON file."""
|
|
11
|
+
path = Path(file_path).expanduser()
|
|
12
|
+
if not path.exists():
|
|
13
|
+
return ReadingList()
|
|
14
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
15
|
+
return ReadingList.from_dict(data)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def save(reading_list: ReadingList, file_path: str) -> None:
|
|
19
|
+
path = Path(file_path).expanduser()
|
|
20
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
payload = json.dumps(reading_list.to_dict(), indent=2)
|
|
22
|
+
path.write_text(payload, encoding="utf-8")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bejw
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A capped reading list for links that shimmer
|
|
5
|
+
Author: menisadi
|
|
6
|
+
Project-URL: Homepage, https://github.com/menisadi/bejw
|
|
7
|
+
Project-URL: Repository, https://github.com/menisadi/bejw
|
|
8
|
+
Project-URL: Changelog, https://github.com/menisadi/bejw/blob/main/CHANGELOG.md
|
|
9
|
+
Keywords: cli,reading-list,links,productivity
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: rich>=14.3.2
|
|
13
|
+
Requires-Dist: typer>=0.21.1
|
|
14
|
+
|
|
15
|
+
# bejw
|
|
16
|
+
|
|
17
|
+
A small CLI to keep a capped reading list of links in a local JSON file.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv sync
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python main.py --help
|
|
29
|
+
python main.py init --capacity 10
|
|
30
|
+
python main.py add "https://example.com" "Example"
|
|
31
|
+
python main.py list
|
|
32
|
+
python main.py remove <id>
|
|
33
|
+
python main.py capacity 5
|
|
34
|
+
python main.py clear
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Storage
|
|
38
|
+
|
|
39
|
+
By default, data is stored in `~/.bejw/links.json`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
bejw/__init__.py
|
|
4
|
+
bejw/main.py
|
|
5
|
+
bejw/models.py
|
|
6
|
+
bejw/render.py
|
|
7
|
+
bejw/storage.py
|
|
8
|
+
bejw.egg-info/PKG-INFO
|
|
9
|
+
bejw.egg-info/SOURCES.txt
|
|
10
|
+
bejw.egg-info/dependency_links.txt
|
|
11
|
+
bejw.egg-info/entry_points.txt
|
|
12
|
+
bejw.egg-info/requires.txt
|
|
13
|
+
bejw.egg-info/top_level.txt
|
|
14
|
+
tests/test_models.py
|
|
15
|
+
tests/test_storage.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bejw
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bejw"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "A capped reading list for links that shimmer"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
authors = [{ name = "menisadi" }]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"rich>=14.3.2",
|
|
14
|
+
"typer>=0.21.1",
|
|
15
|
+
]
|
|
16
|
+
keywords = ["cli", "reading-list", "links", "productivity"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/menisadi/bejw"
|
|
20
|
+
Repository = "https://github.com/menisadi/bejw"
|
|
21
|
+
Changelog = "https://github.com/menisadi/bejw/blob/main/CHANGELOG.md"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
bejw = "bejw.main:app"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=9.0.2",
|
|
29
|
+
]
|
bejw-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from bejw.models import CapacityError, Link, ReadingList
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_add_link_increases_size_and_returns_link() -> None:
|
|
7
|
+
reading_list = ReadingList(capacity=2)
|
|
8
|
+
|
|
9
|
+
link = reading_list.add_link("https://example.com", "Example")
|
|
10
|
+
|
|
11
|
+
assert isinstance(link, Link)
|
|
12
|
+
assert link.url == "https://example.com"
|
|
13
|
+
assert link.title == "Example"
|
|
14
|
+
assert len(reading_list.links) == 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_add_link_raises_when_capacity_reached() -> None:
|
|
18
|
+
reading_list = ReadingList(capacity=1)
|
|
19
|
+
reading_list.add_link("https://first.com", "First")
|
|
20
|
+
|
|
21
|
+
with pytest.raises(CapacityError):
|
|
22
|
+
reading_list.add_link("https://second.com", "Second")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_remove_link_returns_true_when_found() -> None:
|
|
26
|
+
reading_list = ReadingList()
|
|
27
|
+
link = reading_list.add_link("https://example.com", "Example")
|
|
28
|
+
|
|
29
|
+
removed = reading_list.remove_link(link.id)
|
|
30
|
+
|
|
31
|
+
assert removed is True
|
|
32
|
+
assert reading_list.links == []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_remove_link_returns_false_when_missing() -> None:
|
|
36
|
+
reading_list = ReadingList()
|
|
37
|
+
|
|
38
|
+
removed = reading_list.remove_link("missing-id")
|
|
39
|
+
|
|
40
|
+
assert removed is False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_to_dict_and_from_dict_round_trip() -> None:
|
|
44
|
+
original = ReadingList(capacity=3)
|
|
45
|
+
added = original.add_link("https://example.com", "Example")
|
|
46
|
+
|
|
47
|
+
payload = original.to_dict()
|
|
48
|
+
restored = ReadingList.from_dict(payload)
|
|
49
|
+
|
|
50
|
+
assert restored.capacity == 3
|
|
51
|
+
assert len(restored.links) == 1
|
|
52
|
+
assert restored.links[0] == added
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from bejw.models import ReadingList
|
|
4
|
+
from bejw.storage import load, save
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_load_returns_default_when_file_missing(tmp_path: Path) -> None:
|
|
8
|
+
file_path = tmp_path / "links.json"
|
|
9
|
+
|
|
10
|
+
reading_list = load(str(file_path))
|
|
11
|
+
|
|
12
|
+
assert reading_list.capacity == 10
|
|
13
|
+
assert reading_list.links == []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_save_then_load_round_trip(tmp_path: Path) -> None:
|
|
17
|
+
file_path = tmp_path / "nested" / "links.json"
|
|
18
|
+
original = ReadingList(capacity=2)
|
|
19
|
+
original.add_link("https://example.com", "Example")
|
|
20
|
+
|
|
21
|
+
save(original, str(file_path))
|
|
22
|
+
loaded = load(str(file_path))
|
|
23
|
+
|
|
24
|
+
assert loaded.capacity == 2
|
|
25
|
+
assert len(loaded.links) == 1
|
|
26
|
+
assert loaded.links[0].url == "https://example.com"
|
|
27
|
+
assert loaded.links[0].title == "Example"
|