Merge pull request #400 from PR0M3TH3AN/codex/update-entry-management-for-tags-support

Add tag support for entries
This commit is contained in:
thePR0M3TH3AN
2025-07-08 14:08:15 -04:00
committed by GitHub
10 changed files with 143 additions and 6 deletions

View File

@@ -79,6 +79,7 @@ class EntryManager:
if "word_count" not in entry and "words" in entry: if "word_count" not in entry and "words" in entry:
entry["word_count"] = entry["words"] entry["word_count"] = entry["words"]
entry.pop("words", None) entry.pop("words", None)
entry.setdefault("tags", [])
logger.debug("Index loaded successfully.") logger.debug("Index loaded successfully.")
return data return data
except Exception as e: except Exception as e:
@@ -127,6 +128,7 @@ class EntryManager:
archived: bool = False, archived: bool = False,
notes: str = "", notes: str = "",
custom_fields: List[Dict[str, Any]] | None = None, custom_fields: List[Dict[str, Any]] | None = None,
tags: list[str] | None = None,
) -> int: ) -> int:
""" """
Adds a new entry to the encrypted JSON index file. Adds a new entry to the encrypted JSON index file.
@@ -154,6 +156,7 @@ class EntryManager:
"kind": EntryType.PASSWORD.value, "kind": EntryType.PASSWORD.value,
"notes": notes, "notes": notes,
"custom_fields": custom_fields or [], "custom_fields": custom_fields or [],
"tags": tags or [],
} }
logger.debug(f"Added entry at index {index}: {data['entries'][str(index)]}") logger.debug(f"Added entry at index {index}: {data['entries'][str(index)]}")
@@ -197,6 +200,7 @@ class EntryManager:
period: int = 30, period: int = 30,
digits: int = 6, digits: int = 6,
notes: str = "", notes: str = "",
tags: list[str] | None = None,
) -> str: ) -> str:
"""Add a new TOTP entry and return the provisioning URI.""" """Add a new TOTP entry and return the provisioning URI."""
entry_id = self.get_next_index() entry_id = self.get_next_index()
@@ -216,6 +220,7 @@ class EntryManager:
"digits": digits, "digits": digits,
"archived": archived, "archived": archived,
"notes": notes, "notes": notes,
"tags": tags or [],
} }
else: else:
entry = { entry = {
@@ -227,6 +232,7 @@ class EntryManager:
"digits": digits, "digits": digits,
"archived": archived, "archived": archived,
"notes": notes, "notes": notes,
"tags": tags or [],
} }
data["entries"][str(entry_id)] = entry data["entries"][str(entry_id)] = entry
@@ -248,6 +254,7 @@ class EntryManager:
index: int | None = None, index: int | None = None,
notes: str = "", notes: str = "",
archived: bool = False, archived: bool = False,
tags: list[str] | None = None,
) -> int: ) -> int:
"""Add a new SSH key pair entry. """Add a new SSH key pair entry.
@@ -268,6 +275,7 @@ class EntryManager:
"label": label, "label": label,
"notes": notes, "notes": notes,
"archived": archived, "archived": archived,
"tags": tags or [],
} }
self._save_index(data) self._save_index(data)
self.update_checksum() self.update_checksum()
@@ -297,6 +305,7 @@ class EntryManager:
user_id: str = "", user_id: str = "",
notes: str = "", notes: str = "",
archived: bool = False, archived: bool = False,
tags: list[str] | None = None,
) -> int: ) -> int:
"""Add a new PGP key entry.""" """Add a new PGP key entry."""
@@ -314,6 +323,7 @@ class EntryManager:
"user_id": user_id, "user_id": user_id,
"notes": notes, "notes": notes,
"archived": archived, "archived": archived,
"tags": tags or [],
} }
self._save_index(data) self._save_index(data)
self.update_checksum() self.update_checksum()
@@ -347,6 +357,7 @@ class EntryManager:
index: int | None = None, index: int | None = None,
notes: str = "", notes: str = "",
archived: bool = False, archived: bool = False,
tags: list[str] | None = None,
) -> int: ) -> int:
"""Add a new Nostr key pair entry.""" """Add a new Nostr key pair entry."""
@@ -362,6 +373,7 @@ class EntryManager:
"label": label, "label": label,
"notes": notes, "notes": notes,
"archived": archived, "archived": archived,
"tags": tags or [],
} }
self._save_index(data) self._save_index(data)
self.update_checksum() self.update_checksum()
@@ -376,6 +388,7 @@ class EntryManager:
notes: str = "", notes: str = "",
custom_fields=None, custom_fields=None,
archived: bool = False, archived: bool = False,
tags: list[str] | None = None,
) -> int: ) -> int:
"""Add a new generic key/value entry.""" """Add a new generic key/value entry."""
@@ -391,6 +404,7 @@ class EntryManager:
"notes": notes, "notes": notes,
"archived": archived, "archived": archived,
"custom_fields": custom_fields or [], "custom_fields": custom_fields or [],
"tags": tags or [],
} }
self._save_index(data) self._save_index(data)
@@ -431,6 +445,7 @@ class EntryManager:
words_num: int = 24, words_num: int = 24,
notes: str = "", notes: str = "",
archived: bool = False, archived: bool = False,
tags: list[str] | None = None,
) -> int: ) -> int:
"""Add a new derived seed phrase entry.""" """Add a new derived seed phrase entry."""
@@ -447,6 +462,7 @@ class EntryManager:
"word_count": words_num, "word_count": words_num,
"notes": notes, "notes": notes,
"archived": archived, "archived": archived,
"tags": tags or [],
} }
self._save_index(data) self._save_index(data)
self.update_checksum() self.update_checksum()
@@ -483,6 +499,7 @@ class EntryManager:
index: int | None = None, index: int | None = None,
notes: str = "", notes: str = "",
archived: bool = False, archived: bool = False,
tags: list[str] | None = None,
) -> int: ) -> int:
"""Add a new managed account seed entry. """Add a new managed account seed entry.
@@ -518,6 +535,7 @@ class EntryManager:
"notes": notes, "notes": notes,
"fingerprint": fingerprint, "fingerprint": fingerprint,
"archived": archived, "archived": archived,
"tags": tags or [],
} }
self._save_index(data) self._save_index(data)
@@ -642,6 +660,7 @@ class EntryManager:
digits: Optional[int] = None, digits: Optional[int] = None,
value: Optional[str] = None, value: Optional[str] = None,
custom_fields: List[Dict[str, Any]] | None = None, custom_fields: List[Dict[str, Any]] | None = None,
tags: list[str] | None = None,
**legacy, **legacy,
) -> None: ) -> None:
""" """
@@ -727,6 +746,10 @@ class EntryManager:
f"Updated custom fields for index {index}: {custom_fields}" f"Updated custom fields for index {index}: {custom_fields}"
) )
if tags is not None:
entry["tags"] = tags
logger.debug(f"Updated tags for index {index}: {tags}")
data["entries"][str(index)] = entry data["entries"][str(index)] = entry
logger.debug(f"Modified entry at index {index}: {entry}") logger.debug(f"Modified entry at index {index}: {entry}")
@@ -890,8 +913,10 @@ class EntryManager:
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
label = entry.get("label", entry.get("website", "")) label = entry.get("label", entry.get("website", ""))
notes = entry.get("notes", "") notes = entry.get("notes", "")
tags = entry.get("tags", [])
label_match = query_lower in label.lower() label_match = query_lower in label.lower()
notes_match = query_lower in notes.lower() notes_match = query_lower in notes.lower()
tags_match = any(query_lower in str(t).lower() for t in tags)
if etype == EntryType.PASSWORD.value: if etype == EntryType.PASSWORD.value:
username = entry.get("username", "") username = entry.get("username", "")
@@ -908,6 +933,7 @@ class EntryManager:
or query_lower in url.lower() or query_lower in url.lower()
or notes_match or notes_match
or custom_match or custom_match
or tags_match
): ):
results.append( results.append(
( (
@@ -931,6 +957,7 @@ class EntryManager:
or query_lower in value_field.lower() or query_lower in value_field.lower()
or notes_match or notes_match
or custom_match or custom_match
or tags_match
): ):
results.append( results.append(
( (
@@ -942,7 +969,7 @@ class EntryManager:
) )
) )
else: else:
if label_match or notes_match: if label_match or notes_match or tags_match:
results.append( results.append(
( (
int(idx), int(idx),

View File

@@ -981,6 +981,12 @@ class PasswordManager:
username = input("Enter the username (optional): ").strip() username = input("Enter the username (optional): ").strip()
url = input("Enter the URL (optional): ").strip() url = input("Enter the URL (optional): ").strip()
notes = input("Enter notes (optional): ").strip() notes = input("Enter notes (optional): ").strip()
tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
custom_fields: list[dict[str, object]] = [] custom_fields: list[dict[str, object]] = []
while True: while True:
@@ -1021,6 +1027,7 @@ class PasswordManager:
archived=False, archived=False,
notes=notes, notes=notes,
custom_fields=custom_fields, custom_fields=custom_fields,
tags=tags,
) )
# Mark database as dirty for background sync # Mark database as dirty for background sync
@@ -1084,6 +1091,14 @@ class PasswordManager:
) )
continue continue
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
tags_input = input(
"Enter tags (comma-separated, optional): "
).strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
totp_index = self.entry_manager.get_next_totp_index() totp_index = self.entry_manager.get_next_totp_index()
entry_id = self.entry_manager.get_next_index() entry_id = self.entry_manager.get_next_index()
uri = self.entry_manager.add_totp( uri = self.entry_manager.add_totp(
@@ -1093,6 +1108,7 @@ class PasswordManager:
period=int(period), period=int(period),
digits=int(digits), digits=int(digits),
notes=notes, notes=notes,
tags=tags,
) )
secret = TotpManager.derive_secret(self.parent_seed, totp_index) secret = TotpManager.derive_secret(self.parent_seed, totp_index)
self.is_dirty = True self.is_dirty = True
@@ -1128,6 +1144,14 @@ class PasswordManager:
period = int(input("Period (default 30): ").strip() or 30) period = int(input("Period (default 30): ").strip() or 30)
digits = int(input("Digits (default 6): ").strip() or 6) digits = int(input("Digits (default 6): ").strip() or 6)
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
tags_input = input(
"Enter tags (comma-separated, optional): "
).strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
entry_id = self.entry_manager.get_next_index() entry_id = self.entry_manager.get_next_index()
uri = self.entry_manager.add_totp( uri = self.entry_manager.add_totp(
label, label,
@@ -1136,6 +1160,7 @@ class PasswordManager:
period=period, period=period,
digits=digits, digits=digits,
notes=notes, notes=notes,
tags=tags,
) )
self.is_dirty = True self.is_dirty = True
self.last_update = time.time() self.last_update = time.time()
@@ -1181,7 +1206,15 @@ class PasswordManager:
print(colored("Error: Label cannot be empty.", "red")) print(colored("Error: Label cannot be empty.", "red"))
return return
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
index = self.entry_manager.add_ssh_key(label, self.parent_seed, notes=notes) tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
index = self.entry_manager.add_ssh_key(
label, self.parent_seed, notes=notes, tags=tags
)
priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair( priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair(
index, self.parent_seed index, self.parent_seed
) )
@@ -1230,12 +1263,18 @@ class PasswordManager:
return return
words_input = input("Word count (12 or 24, default 24): ").strip() words_input = input("Word count (12 or 24, default 24): ").strip()
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
if words_input and words_input not in {"12", "24"}: if words_input and words_input not in {"12", "24"}:
print(colored("Invalid word count. Choose 12 or 24.", "red")) print(colored("Invalid word count. Choose 12 or 24.", "red"))
return return
words = int(words_input) if words_input else 24 words = int(words_input) if words_input else 24
index = self.entry_manager.add_seed( index = self.entry_manager.add_seed(
label, self.parent_seed, words_num=words, notes=notes label, self.parent_seed, words_num=words, notes=notes, tags=tags
) )
phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed) phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed)
self.is_dirty = True self.is_dirty = True
@@ -1296,12 +1335,19 @@ class PasswordManager:
) )
user_id = input("User ID (optional): ").strip() user_id = input("User ID (optional): ").strip()
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
index = self.entry_manager.add_pgp_key( index = self.entry_manager.add_pgp_key(
label, label,
self.parent_seed, self.parent_seed,
key_type=key_type, key_type=key_type,
user_id=user_id, user_id=user_id,
notes=notes, notes=notes,
tags=tags,
) )
priv_key, fingerprint = self.entry_manager.get_pgp_key( priv_key, fingerprint = self.entry_manager.get_pgp_key(
index, self.parent_seed index, self.parent_seed
@@ -1350,7 +1396,13 @@ class PasswordManager:
print(colored("Error: Label cannot be empty.", "red")) print(colored("Error: Label cannot be empty.", "red"))
return return
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
index = self.entry_manager.add_nostr_key(label, notes=notes) tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
index = self.entry_manager.add_nostr_key(label, notes=notes, tags=tags)
npub, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed) npub, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed)
self.is_dirty = True self.is_dirty = True
self.last_update = time.time() self.last_update = time.time()
@@ -1401,6 +1453,12 @@ class PasswordManager:
return return
value = input("Value: ").strip() value = input("Value: ").strip()
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
custom_fields: list[dict[str, object]] = [] custom_fields: list[dict[str, object]] = []
while True: while True:
@@ -1419,7 +1477,11 @@ class PasswordManager:
) )
index = self.entry_manager.add_key_value( index = self.entry_manager.add_key_value(
label, value, notes=notes, custom_fields=custom_fields label,
value,
notes=notes,
custom_fields=custom_fields,
tags=tags,
) )
self.is_dirty = True self.is_dirty = True
self.last_update = time.time() self.last_update = time.time()
@@ -1465,8 +1527,14 @@ class PasswordManager:
print(colored("Error: Label cannot be empty.", "red")) print(colored("Error: Label cannot be empty.", "red"))
return return
notes = input("Notes (optional): ").strip() notes = input("Notes (optional): ").strip()
tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
index = self.entry_manager.add_managed_account( index = self.entry_manager.add_managed_account(
label, self.parent_seed, notes=notes label, self.parent_seed, notes=notes, tags=tags
) )
seed = self.entry_manager.get_managed_account_seed(index, self.parent_seed) seed = self.entry_manager.get_managed_account_seed(index, self.parent_seed)
self.is_dirty = True self.is_dirty = True
@@ -2190,6 +2258,15 @@ class PasswordManager:
{"label": label, "value": value, "is_hidden": hidden} {"label": label, "value": value, "is_hidden": hidden}
) )
tags_input = input(
"Enter tags (comma-separated, leave blank to keep current): "
).strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else None
)
self.entry_manager.modify_entry( self.entry_manager.modify_entry(
index, index,
archived=new_blacklisted, archived=new_blacklisted,
@@ -2198,6 +2275,7 @@ class PasswordManager:
period=new_period, period=new_period,
digits=new_digits, digits=new_digits,
custom_fields=custom_fields, custom_fields=custom_fields,
tags=tags,
) )
elif entry_type in ( elif entry_type in (
EntryType.KEY_VALUE.value, EntryType.KEY_VALUE.value,
@@ -2273,6 +2351,15 @@ class PasswordManager:
{"label": f_label, "value": f_value, "is_hidden": hidden} {"label": f_label, "value": f_value, "is_hidden": hidden}
) )
tags_input = input(
"Enter tags (comma-separated, leave blank to keep current): "
).strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else None
)
self.entry_manager.modify_entry( self.entry_manager.modify_entry(
index, index,
archived=new_blacklisted, archived=new_blacklisted,
@@ -2280,6 +2367,7 @@ class PasswordManager:
label=new_label, label=new_label,
value=new_value, value=new_value,
custom_fields=custom_fields, custom_fields=custom_fields,
tags=tags,
) )
else: else:
website_name = entry.get("label", entry.get("website")) website_name = entry.get("label", entry.get("website"))
@@ -2366,6 +2454,15 @@ class PasswordManager:
{"label": label, "value": value, "is_hidden": hidden} {"label": label, "value": value, "is_hidden": hidden}
) )
tags_input = input(
"Enter tags (comma-separated, leave blank to keep current): "
).strip()
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else None
)
self.entry_manager.modify_entry( self.entry_manager.modify_entry(
index, index,
new_username, new_username,
@@ -2374,6 +2471,7 @@ class PasswordManager:
notes=new_notes, notes=new_notes,
label=new_label, label=new_label,
custom_fields=custom_fields, custom_fields=custom_fields,
tags=tags,
) )
# Mark database as dirty for background sync # Mark database as dirty for background sync

View File

@@ -39,6 +39,7 @@ def test_add_and_retrieve_entry():
"kind": "password", "kind": "password",
"notes": "", "notes": "",
"custom_fields": custom, "custom_fields": custom,
"tags": [],
} }
data = enc_mgr.load_json_data(entry_mgr.index_file) data = enc_mgr.load_json_data(entry_mgr.index_file)

View File

@@ -33,6 +33,7 @@ def test_add_and_modify_key_value():
"notes": "token", "notes": "token",
"archived": False, "archived": False,
"custom_fields": [], "custom_fields": [],
"tags": [],
} }
em.modify_entry(idx, value="def456") em.modify_entry(idx, value="def456")

View File

@@ -48,6 +48,7 @@ def test_handle_add_totp(monkeypatch, capsys):
"", # period "", # period
"", # digits "", # digits
"", # notes "", # notes
"", # tags
] ]
) )
monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs)) monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs))
@@ -66,5 +67,6 @@ def test_handle_add_totp(monkeypatch, capsys):
"digits": 6, "digits": 6,
"archived": False, "archived": False,
"notes": "", "notes": "",
"tags": [],
} }
assert "ID 0" in out assert "ID 0" in out

View File

@@ -54,6 +54,7 @@ def test_manager_workflow(monkeypatch):
"", # username "", # username
"", # url "", # url
"", # notes "", # notes
"", # tags
"n", # add custom field "n", # add custom field
"", # length (default) "", # length (default)
"0", # retrieve index "0", # retrieve index
@@ -65,6 +66,7 @@ def test_manager_workflow(monkeypatch):
"", # archive status "", # archive status
"", # new notes "", # new notes
"n", # edit custom fields "n", # edit custom fields
"", # tags keep
] ]
) )
monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs)) monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs))

View File

@@ -31,6 +31,7 @@ def test_nostr_key_determinism():
"label": "main", "label": "main",
"notes": "", "notes": "",
"archived": False, "archived": False,
"tags": [],
} }
npub1, nsec1 = entry_mgr.get_nostr_key_pair(idx, TEST_SEED) npub1, nsec1 = entry_mgr.get_nostr_key_pair(idx, TEST_SEED)

View File

@@ -42,6 +42,7 @@ def test_seed_phrase_determinism():
"word_count": 12, "word_count": 12,
"notes": "", "notes": "",
"archived": False, "archived": False,
"tags": [],
} }
assert entry24 == { assert entry24 == {
@@ -52,6 +53,7 @@ def test_seed_phrase_determinism():
"word_count": 24, "word_count": 24,
"notes": "", "notes": "",
"archived": False, "archived": False,
"tags": [],
} }
assert phrase12_a not in entry12.values() assert phrase12_a not in entry12.values()

View File

@@ -29,6 +29,7 @@ def test_add_and_retrieve_ssh_key_pair():
"label": "ssh", "label": "ssh",
"notes": "", "notes": "",
"archived": False, "archived": False,
"tags": [],
} }
priv1, pub1 = entry_mgr.get_ssh_key_pair(index, TEST_SEED) priv1, pub1 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)

View File

@@ -37,6 +37,7 @@ def test_add_totp_and_get_code():
"digits": 6, "digits": 6,
"archived": False, "archived": False,
"notes": "", "notes": "",
"tags": [],
} }
code = entry_mgr.get_totp_code(0, TEST_SEED, timestamp=0) code = entry_mgr.get_totp_code(0, TEST_SEED, timestamp=0)
@@ -76,6 +77,7 @@ def test_add_totp_imported(tmp_path):
"digits": 6, "digits": 6,
"archived": False, "archived": False,
"notes": "", "notes": "",
"tags": [],
} }
code = em.get_totp_code(0, timestamp=0) code = em.get_totp_code(0, timestamp=0)
assert code == pyotp.TOTP(secret).at(0) assert code == pyotp.TOTP(secret).at(0)