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 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
@@ -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,2 @@
1
+ [console_scripts]
2
+ bejw = bejw.main:app
@@ -0,0 +1,2 @@
1
+ rich>=14.3.2
2
+ typer>=0.21.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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"