Merge pull request #824 from PR0M3TH3AN/codex/add-all_entry_types-constant-and-update-filters

Support listing all entry types
This commit is contained in:
thePR0M3TH3AN
2025-08-18 19:22:05 -04:00
committed by GitHub
13 changed files with 142 additions and 54 deletions

View File

@@ -478,7 +478,7 @@ def get_totp_codes(
_require_password(request, password) _require_password(request, password)
pm = _get_pm(request) pm = _get_pm(request)
entries = pm.entry_manager.list_entries( entries = pm.entry_manager.list_entries(
filter_kind=EntryType.TOTP.value, include_archived=False filter_kinds=[EntryType.TOTP.value], include_archived=False
) )
codes = [] codes = []
for idx, label, _u, _url, _arch in entries: for idx, label, _u, _url, _arch in entries:

View File

@@ -6,8 +6,10 @@ from pathlib import Path
from typing import List, Optional from typing import List, Optional
import typer import typer
import click
from .common import _get_entry_service, EntryType from .common import _get_entry_service, EntryType
from seedpass.core.entry_types import ALL_ENTRY_TYPES
from utils.clipboard import ClipboardUnavailableError from utils.clipboard import ClipboardUnavailableError
@@ -20,13 +22,20 @@ def entry_list(
sort: str = typer.Option( sort: str = typer.Option(
"index", "--sort", help="Sort by 'index', 'label', or 'updated'" "index", "--sort", help="Sort by 'index', 'label', or 'updated'"
), ),
kind: Optional[str] = typer.Option(None, "--kind", help="Filter by entry type"), kind: Optional[str] = typer.Option(
None,
"--kind",
help="Filter by entry type",
click_type=click.Choice(ALL_ENTRY_TYPES),
),
archived: bool = typer.Option(False, "--archived", help="Include archived"), archived: bool = typer.Option(False, "--archived", help="Include archived"),
) -> None: ) -> None:
"""List entries in the vault.""" """List entries in the vault."""
service = _get_entry_service(ctx) service = _get_entry_service(ctx)
entries = service.list_entries( entries = service.list_entries(
sort_by=sort, filter_kind=kind, include_archived=archived sort_by=sort,
filter_kinds=[kind] if kind else None,
include_archived=archived,
) )
for idx, label, username, url, is_archived in entries: for idx, label, username, url, is_archived in entries:
line = f"{idx}: {label}" line = f"{idx}: {label}"
@@ -43,16 +52,17 @@ def entry_list(
def entry_search( def entry_search(
ctx: typer.Context, ctx: typer.Context,
query: str, query: str,
kind: List[str] = typer.Option( kinds: List[str] = typer.Option(
None, None,
"--kind", "--kind",
"-k", "-k",
help="Filter by entry kinds (can be repeated)", help="Filter by entry kinds (can be repeated)",
click_type=click.Choice(ALL_ENTRY_TYPES),
), ),
) -> None: ) -> None:
"""Search entries.""" """Search entries."""
service = _get_entry_service(ctx) service = _get_entry_service(ctx)
kinds = list(kind) if kind else None kinds = list(kinds) if kinds else None
results = service.search_entries(query, kinds=kinds) results = service.search_entries(query, kinds=kinds)
if not results: if not results:
typer.echo("No matching entries found") typer.echo("No matching entries found")

View File

@@ -265,13 +265,13 @@ class EntryService:
def list_entries( def list_entries(
self, self,
sort_by: str = "index", sort_by: str = "index",
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
include_archived: bool = False, include_archived: bool = False,
): ):
with self._lock: with self._lock:
return self._manager.entry_manager.list_entries( return self._manager.entry_manager.list_entries(
sort_by=sort_by, sort_by=sort_by,
filter_kind=filter_kind, filter_kinds=filter_kinds,
include_archived=include_archived, include_archived=include_archived,
) )

View File

@@ -33,7 +33,7 @@ from pathlib import Path
from termcolor import colored from termcolor import colored
from .migrations import LATEST_VERSION from .migrations import LATEST_VERSION
from .entry_types import EntryType from .entry_types import EntryType, ALL_ENTRY_TYPES
from .totp import TotpManager from .totp import TotpManager
from utils.fingerprint import generate_fingerprint from utils.fingerprint import generate_fingerprint
from utils.checksum import canonical_json_dumps from utils.checksum import canonical_json_dumps
@@ -1076,7 +1076,7 @@ class EntryManager:
def list_entries( def list_entries(
self, self,
sort_by: str = "index", sort_by: str = "index",
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
*, *,
include_archived: bool = False, include_archived: bool = False,
verbose: bool = True, verbose: bool = True,
@@ -1088,8 +1088,9 @@ class EntryManager:
sort_by: sort_by:
Field to sort by. Supported values are ``"index"``, ``"label"`` and Field to sort by. Supported values are ``"index"``, ``"label"`` and
``"updated"``. ``"updated"``.
filter_kind: filter_kinds:
Optional entry kind to restrict the results. Optional list of entry kinds to restrict the results. Defaults to
``ALL_ENTRY_TYPES``.
Archived entries are omitted unless ``include_archived`` is ``True``. Archived entries are omitted unless ``include_archived`` is ``True``.
""" """
@@ -1118,12 +1119,14 @@ class EntryManager:
sorted_items = sorted(entries_data.items(), key=sort_key) sorted_items = sorted(entries_data.items(), key=sort_key)
if filter_kinds is None:
filter_kinds = ALL_ENTRY_TYPES
filtered_items: List[Tuple[int, Dict[str, Any]]] = [] filtered_items: List[Tuple[int, Dict[str, Any]]] = []
for idx_str, entry in sorted_items: for idx_str, entry in sorted_items:
if ( if (
filter_kind is not None entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
and entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) not in filter_kinds
!= filter_kind
): ):
continue continue
if not include_archived and entry.get( if not include_archived and entry.get(
@@ -1371,7 +1374,7 @@ class EntryManager:
def list_all_entries( def list_all_entries(
self, self,
sort_by: str = "index", sort_by: str = "index",
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
*, *,
include_archived: bool = False, include_archived: bool = False,
) -> None: ) -> None:
@@ -1379,7 +1382,7 @@ class EntryManager:
try: try:
entries = self.list_entries( entries = self.list_entries(
sort_by=sort_by, sort_by=sort_by,
filter_kind=filter_kind, filter_kinds=filter_kinds,
include_archived=include_archived, include_archived=include_archived,
) )
if not entries: if not entries:
@@ -1403,7 +1406,7 @@ class EntryManager:
def get_entry_summaries( def get_entry_summaries(
self, self,
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
*, *,
include_archived: bool = False, include_archived: bool = False,
) -> list[tuple[int, str, str]]: ) -> list[tuple[int, str, str]]:
@@ -1412,10 +1415,13 @@ class EntryManager:
data = self._load_index() data = self._load_index()
entries_data = data.get("entries", {}) entries_data = data.get("entries", {})
if filter_kinds is None:
filter_kinds = ALL_ENTRY_TYPES
summaries: list[tuple[int, str, str]] = [] summaries: list[tuple[int, str, str]] = []
for idx_str, entry in entries_data.items(): for idx_str, entry in entries_data.items():
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
if filter_kind and etype != filter_kind: if etype not in filter_kinds:
continue continue
if not include_archived and entry.get( if not include_archived and entry.get(
"archived", entry.get("blacklisted", False) "archived", entry.get("blacklisted", False)

View File

@@ -15,3 +15,7 @@ class EntryType(str, Enum):
NOSTR = "nostr" NOSTR = "nostr"
KEY_VALUE = "key_value" KEY_VALUE = "key_value"
MANAGED_ACCOUNT = "managed_account" MANAGED_ACCOUNT = "managed_account"
# List of all entry type values for convenience
ALL_ENTRY_TYPES = [e.value for e in EntryType]

View File

@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
from termcolor import colored from termcolor import colored
from .entry_types import EntryType from .entry_types import EntryType, ALL_ENTRY_TYPES
import seedpass.core.manager as manager_module import seedpass.core.manager as manager_module
from utils.color_scheme import color_text from utils.color_scheme import color_text
from utils.terminal_utils import clear_header_with_notification from utils.terminal_utils import clear_header_with_notification
@@ -36,33 +36,16 @@ class MenuHandler:
) )
print(color_text("\nList Entries:", "menu")) print(color_text("\nList Entries:", "menu"))
print(color_text("1. All", "menu")) print(color_text("1. All", "menu"))
print(color_text("2. Passwords", "menu")) option_map: dict[str, str] = {}
print(color_text("3. 2FA (TOTP)", "menu")) for i, etype in enumerate(ALL_ENTRY_TYPES, start=2):
print(color_text("4. SSH Key", "menu")) label = etype.replace("_", " ").title()
print(color_text("5. Seed Phrase", "menu")) print(color_text(f"{i}. {label}", "menu"))
print(color_text("6. Nostr Key Pair", "menu")) option_map[str(i)] = etype
print(color_text("7. PGP", "menu"))
print(color_text("8. Key/Value", "menu"))
print(color_text("9. Managed Account", "menu"))
choice = input("Select entry type or press Enter to go back: ").strip() choice = input("Select entry type or press Enter to go back: ").strip()
if choice == "1": if choice == "1":
filter_kind = None filter_kinds = None
elif choice == "2": elif choice in option_map:
filter_kind = EntryType.PASSWORD.value filter_kinds = [option_map[choice]]
elif choice == "3":
filter_kind = EntryType.TOTP.value
elif choice == "4":
filter_kind = EntryType.SSH.value
elif choice == "5":
filter_kind = EntryType.SEED.value
elif choice == "6":
filter_kind = EntryType.NOSTR.value
elif choice == "7":
filter_kind = EntryType.PGP.value
elif choice == "8":
filter_kind = EntryType.KEY_VALUE.value
elif choice == "9":
filter_kind = EntryType.MANAGED_ACCOUNT.value
elif not choice: elif not choice:
return return
else: else:
@@ -71,7 +54,7 @@ class MenuHandler:
while True: while True:
summaries = pm.entry_manager.get_entry_summaries( summaries = pm.entry_manager.get_entry_summaries(
filter_kind, include_archived=False filter_kinds, include_archived=False
) )
if not summaries: if not summaries:
break break
@@ -85,7 +68,7 @@ class MenuHandler:
) )
print(colored("\n[+] Entries:\n", "green")) print(colored("\n[+] Entries:\n", "green"))
for idx, etype, label in summaries: for idx, etype, label in summaries:
if filter_kind is None: if filter_kinds is None:
display_type = etype.capitalize() display_type = etype.capitalize()
print(colored(f"{idx}. {display_type} - {label}", "cyan")) print(colored(f"{idx}. {display_type} - {label}", "cyan"))
else: else:

View File

@@ -393,7 +393,7 @@ class TotpViewerWindow(toga.Window):
def refresh_codes(self) -> None: def refresh_codes(self) -> None:
self.table.data = [] self.table.data = []
for idx, label, *_rest in self.entries.list_entries( for idx, label, *_rest in self.entries.list_entries(
filter_kind=EntryType.TOTP.value filter_kinds=[EntryType.TOTP.value]
): ):
entry = self.entries.retrieve_entry(idx) entry = self.entries.retrieve_entry(idx)
code = self.entries.get_totp_code(idx) code = self.entries.get_totp_code(idx)

View File

@@ -16,7 +16,7 @@ from seedpass.core.entry_types import EntryType
class DummyPM: class DummyPM:
def __init__(self): def __init__(self):
self.entry_manager = SimpleNamespace( self.entry_manager = SimpleNamespace(
list_entries=lambda sort_by="index", filter_kind=None, include_archived=False: [ list_entries=lambda sort_by="index", filter_kinds=None, include_archived=False: [
(1, "Label", "user", "url", False) (1, "Label", "user", "url", False)
], ],
search_entries=lambda q, kinds=None: [ search_entries=lambda q, kinds=None: [

View File

@@ -30,8 +30,8 @@ class DummyEntries:
self.data = [(1, "Example", None, None, False)] self.data = [(1, "Example", None, None, False)]
self.code = "111111" self.code = "111111"
def list_entries(self, sort_by="index", filter_kind=None, include_archived=False): def list_entries(self, sort_by="index", filter_kinds=None, include_archived=False):
if filter_kind: if filter_kinds:
return [(idx, label, None, None, False) for idx, label, *_ in self.data] return [(idx, label, None, None, False) for idx, label, *_ in self.data]
return self.data return self.data

View File

@@ -9,7 +9,7 @@ from seedpass_gui.app import MainWindow
class DummyEntries: class DummyEntries:
def list_entries(self, sort_by="index", filter_kind=None, include_archived=False): def list_entries(self, sort_by="index", filter_kinds=None, include_archived=False):
return [] return []
def search_entries(self, q): def search_entries(self, q):

View File

@@ -0,0 +1,85 @@
from __future__ import annotations
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from typer.testing import CliRunner
from seedpass.cli import app as cli_app
from seedpass.cli import entry as entry_cli
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager
from seedpass.core.entry_management import EntryManager
from seedpass.core.manager import PasswordManager, EncryptionMode
def _setup_manager(tmp_path: Path) -> tuple[PasswordManager, EntryManager]:
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path)
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
pm = PasswordManager.__new__(PasswordManager)
pm.encryption_mode = EncryptionMode.SEED_ONLY
pm.encryption_manager = enc_mgr
pm.vault = vault
pm.entry_manager = entry_mgr
pm.backup_manager = backup_mgr
pm.parent_seed = TEST_SEED
pm.nostr_client = SimpleNamespace()
pm.fingerprint_dir = tmp_path
pm.secret_mode_enabled = False
return pm, entry_mgr
def _create_all_entries(em: EntryManager) -> None:
em.add_entry("pw", 8)
em.add_totp("totp", TEST_SEED)
em.add_ssh_key("ssh", TEST_SEED)
em.add_seed("seed", TEST_SEED, words_num=12)
em.add_nostr_key("nostr", TEST_SEED)
em.add_pgp_key("pgp", TEST_SEED)
em.add_key_value("kv", "k", "v")
em.add_managed_account("acct", TEST_SEED)
def test_cli_list_all_types(monkeypatch):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, em = _setup_manager(tmp_path)
_create_all_entries(em)
def fake_get_entry_service(_ctx):
return SimpleNamespace(
list_entries=lambda sort_by, filter_kinds, include_archived: pm.entry_manager.list_entries(
sort_by=sort_by,
filter_kinds=filter_kinds,
include_archived=include_archived,
)
)
monkeypatch.setattr(entry_cli, "_get_entry_service", fake_get_entry_service)
runner = CliRunner()
result = runner.invoke(cli_app, ["entry", "list"])
assert result.exit_code == 0
out = result.stdout
for label in ["pw", "totp", "ssh", "seed", "nostr", "pgp", "kv", "acct"]:
assert label in out
def test_menu_list_all_types(monkeypatch, capsys):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, em = _setup_manager(tmp_path)
_create_all_entries(em)
inputs = iter(["1", "", ""]) # choose All then exit
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
pm.handle_list_entries()
out = capsys.readouterr().out
for label in ["pw", "totp", "ssh", "seed", "nostr", "pgp", "kv", "acct"]:
assert label in out

View File

@@ -57,5 +57,5 @@ def test_filter_by_type():
em = setup_entry_manager(tmp_path) em = setup_entry_manager(tmp_path)
em.add_entry("site", 8, "user") em.add_entry("site", 8, "user")
em.add_totp("Example", TEST_SEED) em.add_totp("Example", TEST_SEED)
result = em.list_entries(filter_kind=EntryType.TOTP.value) result = em.list_entries(filter_kinds=[EntryType.TOTP.value])
assert result == [(1, "Example", None, None, False)] assert result == [(1, "Example", None, None, False)]

View File

@@ -18,8 +18,8 @@ runner = CliRunner()
def test_entry_list(monkeypatch): def test_entry_list(monkeypatch):
called = {} called = {}
def list_entries(sort_by="index", filter_kind=None, include_archived=False): def list_entries(sort_by="index", filter_kinds=None, include_archived=False):
called["args"] = (sort_by, filter_kind, include_archived) called["args"] = (sort_by, filter_kinds, include_archived)
return [(0, "Site", "user", "", False)] return [(0, "Site", "user", "", False)]
pm = SimpleNamespace( pm = SimpleNamespace(