feat: add import command
This commit is contained in:
parent
d37bffdc51
commit
be20284f78
9 changed files with 133 additions and 10 deletions
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.3.1"
|
__version__ = "0.3.2"
|
||||||
|
|
|
||||||
41
halig/commands/import_unencrypted.py
Normal file
41
halig/commands/import_unencrypted.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from collections.abc import Generator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from halig.commands.base import BaseCommand
|
||||||
|
from halig.encryption import Encryptor
|
||||||
|
from halig.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class ImportCommand(BaseCommand):
|
||||||
|
def __init__(self, settings: Settings, unlink: bool = False):
|
||||||
|
super().__init__(settings)
|
||||||
|
self.encryptor = Encryptor(self.settings)
|
||||||
|
self.unlink = unlink
|
||||||
|
|
||||||
|
def get_importables(self) -> Generator[Path, None, None]:
|
||||||
|
"""Get all markdown files under self.settings.notebooks_root_path
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
a list of Path objects matching any markdown files under
|
||||||
|
the notebooks root path
|
||||||
|
"""
|
||||||
|
return self.settings.notebooks_root_path.glob("**/*.md")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""For every importable file, encrypt its contents inside
|
||||||
|
`filename.age`. If `self.unlink` is `True`, unlink the original
|
||||||
|
file
|
||||||
|
"""
|
||||||
|
|
||||||
|
for file_path in self.get_importables():
|
||||||
|
with file_path.open("rb") as f:
|
||||||
|
contents = f.read()
|
||||||
|
|
||||||
|
encrypted_contents = self.encryptor.encrypt(contents)
|
||||||
|
encrypted_file_path = file_path.with_suffix("").with_suffix(".age")
|
||||||
|
encrypted_file_path.touch()
|
||||||
|
with encrypted_file_path.open("wb") as f:
|
||||||
|
f.write(encrypted_contents)
|
||||||
|
|
||||||
|
if self.unlink:
|
||||||
|
file_path.unlink()
|
||||||
|
|
@ -3,13 +3,16 @@ COMMANDS_NOTEBOOKS_HELP = "List all notebooks and notes, tree-style"
|
||||||
COMMANDS_EDIT_HELP = "Edit or add a note into a notebook"
|
COMMANDS_EDIT_HELP = "Edit or add a note into a notebook"
|
||||||
COMMANDS_SHOW_HELP = "Show a note's contents"
|
COMMANDS_SHOW_HELP = "Show a note's contents"
|
||||||
COMMANDS_VERSION = "Show halig's version"
|
COMMANDS_VERSION = "Show halig's version"
|
||||||
|
COMMANDS_IMPORT_HELP = "Encrypt existing unencrypted files"
|
||||||
|
|
||||||
# OPTIONS
|
# OPTIONS
|
||||||
OPTION_CONFIG_HELP = "Configuration file. Must be YAML and schema compatible"
|
OPTION_CONFIG_HELP = "Configuration file. Must be YAML and schema compatible"
|
||||||
OPTION_LEVEL_HELP = (
|
OPTION_LEVEL_HELP = (
|
||||||
"Tree max recursion level; negative numbers indicate a value of infinity"
|
"Tree max recursion level; negative numbers indicate a value of infinity"
|
||||||
)
|
)
|
||||||
|
OPTION_UNLINK_HELP = """Setting this will remove the original markdown files;
|
||||||
|
only the newly encrypted .age files will be preserved. Backup your data first
|
||||||
|
"""
|
||||||
# ARGUMENTS
|
# ARGUMENTS
|
||||||
ARGUMENT_EDIT_NOTE_HELP = """A valid, settings-relative path.
|
ARGUMENT_EDIT_NOTE_HELP = """A valid, settings-relative path.
|
||||||
Be aware that valid can also mean implicit notes, that is, pointing to a
|
Be aware that valid can also mean implicit notes, that is, pointing to a
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
from rich.prompt import Prompt
|
||||||
from typer import Argument, Option, Typer
|
from typer import Argument, Option, Typer
|
||||||
|
|
||||||
from halig import literals
|
from halig import literals
|
||||||
from halig.__version__ import __version__
|
from halig.__version__ import __version__
|
||||||
from halig.commands.edit import EditCommand
|
from halig.commands.edit import EditCommand
|
||||||
|
from halig.commands.import_unencrypted import ImportCommand
|
||||||
from halig.commands.notebooks import NotebooksCommand
|
from halig.commands.notebooks import NotebooksCommand
|
||||||
from halig.commands.show import ShowCommand
|
from halig.commands.show import ShowCommand
|
||||||
from halig.settings import load_from_file
|
from halig.settings import load_from_file
|
||||||
|
|
@ -87,6 +89,31 @@ def show(
|
||||||
command.run()
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(name="import", help=literals.COMMANDS_IMPORT_HELP)
|
||||||
|
@capture
|
||||||
|
def import_unencrypted(
|
||||||
|
unlink: bool = Option( # noqa: B008
|
||||||
|
False,
|
||||||
|
help=literals.OPTION_UNLINK_HELP,
|
||||||
|
),
|
||||||
|
config: Optional[Path] = config_option, # noqa: UP007
|
||||||
|
):
|
||||||
|
settings = load_from_file(config)
|
||||||
|
command = ImportCommand(settings=settings, unlink=unlink)
|
||||||
|
files_to_unlink = list(command.get_importables())
|
||||||
|
if files_to_unlink:
|
||||||
|
if unlink:
|
||||||
|
should_unlink = Prompt.ask(
|
||||||
|
f"""Unlink flag set, will delete {len(files_to_unlink)} files.
|
||||||
|
Have you backed up your data?""",
|
||||||
|
choices=["y", "Y", "N", "n"],
|
||||||
|
)
|
||||||
|
if should_unlink in ["N", "n"]:
|
||||||
|
command.unlink = False
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
@app.command(help=literals.COMMANDS_VERSION)
|
@app.command(help=literals.COMMANDS_VERSION)
|
||||||
@capture
|
@capture
|
||||||
def version():
|
def version():
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,7 @@ class Settings(BaseSettings):
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def load_from_file(file_path: Path | None = None) -> Settings:
|
def load_from_file(file_path: Path | None = None) -> Settings:
|
||||||
if file_path is None:
|
if file_path is None:
|
||||||
halig_config_home = Path(
|
halig_config_home = platformdirs.user_config_path("halig", ensure_exists=True)
|
||||||
platformdirs.user_config_dir("halig", ensure_exists=True),
|
|
||||||
)
|
|
||||||
file_path = halig_config_home / "halig.yml"
|
file_path = halig_config_home / "halig.yml"
|
||||||
file_path.touch(exist_ok=True)
|
file_path.touch(exist_ok=True)
|
||||||
elif not file_path.exists():
|
elif not file_path.exists():
|
||||||
|
|
|
||||||
8
pdm.lock
generated
8
pdm.lock
generated
|
|
@ -243,7 +243,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocs-material"
|
name = "mkdocs-material"
|
||||||
version = "9.1.12"
|
version = "9.1.13"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Documentation that simply works"
|
summary = "Documentation that simply works"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
@ -933,9 +933,9 @@ content_hash = "sha256:e698ce4565363b2b89ae66ebc3031f378848822332fe4f2a49019dbb0
|
||||||
{url = "https://files.pythonhosted.org/packages/3b/3f/9531888bc92bafb1bffddca5d9240a7bae9a479d465528883b61808ef9d6/mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
|
{url = "https://files.pythonhosted.org/packages/3b/3f/9531888bc92bafb1bffddca5d9240a7bae9a479d465528883b61808ef9d6/mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
|
||||||
{url = "https://files.pythonhosted.org/packages/fb/5c/6594400290df38f99bf8d9ef249387b56f4ad962667836266f6fe7da8597/mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
|
{url = "https://files.pythonhosted.org/packages/fb/5c/6594400290df38f99bf8d9ef249387b56f4ad962667836266f6fe7da8597/mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
|
||||||
]
|
]
|
||||||
"mkdocs-material 9.1.12" = [
|
"mkdocs-material 9.1.13" = [
|
||||||
{url = "https://files.pythonhosted.org/packages/03/b5/60bbe928cde37b72908bfd25eed76738141edc3ee983eb57e26a53930dbc/mkdocs_material-9.1.12.tar.gz", hash = "sha256:d4ebe9b5031ce63a265c19fb5eab4d27ea4edadb05de206372e831b2b7570fb5"},
|
{url = "https://files.pythonhosted.org/packages/10/10/7245bfb3ae9fe673cb283259a049e75d622418fbdb1cc54d4eef77cce92f/mkdocs_material-9.1.13.tar.gz", hash = "sha256:9102e7604d73e507021847601b0a8b4fe9035422788390183f464fa3b30dd508"},
|
||||||
{url = "https://files.pythonhosted.org/packages/57/51/090775b6b334f63cb16a02d82dc76ab46fcf7798a6874d22136ffe88e5ee/mkdocs_material-9.1.12-py3-none-any.whl", hash = "sha256:68c57d95d10104179c8c3ce9a88ee9d2322a5145b3d0f1f38ff686253fb5ec98"},
|
{url = "https://files.pythonhosted.org/packages/5a/a3/00aaf7d83d44d1bac139df38d0dd30e55b799d13f565b6947f5b7a9e6c0f/mkdocs_material-9.1.13-py3-none-any.whl", hash = "sha256:5705cf8cb6c47c747606bd914bb6c01993ff141295cd259475559a1f09f07d5d"},
|
||||||
]
|
]
|
||||||
"mkdocs-material-extensions 1.1.1" = [
|
"mkdocs-material-extensions 1.1.1" = [
|
||||||
{url = "https://files.pythonhosted.org/packages/cd/3f/e5e3c9bfbb42e4cb661f71bcec787ae6bdf4a161b8c4bb68fd7d991c436c/mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"},
|
{url = "https://files.pythonhosted.org/packages/cd/3f/e5e3c9bfbb42e4cb661f71bcec787ae6bdf4a161b8c4bb68fd7d991c436c/mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"},
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ reportMissingImports = false
|
||||||
reportMissingTypeStubs = false
|
reportMissingTypeStubs = false
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
extend-select = ["W", "C90", "I", "N", "UP", "S", "BLE", "FBT", "B", "A", "COM", "C4", "DTZ", "T10", "EM", "ISC", "T20", "PT", "RSE", "RET", "SIM", "PTH", "ERA", "PGH", "PL", "TRY", "RUF"]
|
extend-select = ["W", "C90", "I", "N", "UP", "S", "BLE", "B", "A", "COM", "C4", "DTZ", "T10", "EM", "ISC", "T20", "PT", "RSE", "RET", "SIM", "PTH", "ERA", "PGH", "PL", "TRY", "RUF"]
|
||||||
extend-ignore = ["S101", "ISC002"]
|
extend-ignore = ["S101", "ISC002"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,24 @@ def notes(notebooks_path: Path):
|
||||||
dt = dt.subtract(days=day_offset)
|
dt = dt.subtract(days=day_offset)
|
||||||
(dailies / f"{dt.date()}.age").touch()
|
(dailies / f"{dt.date()}.age").touch()
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def unencrypted_notes(notebooks_path):
|
||||||
|
unencrypted_root_path = notebooks_path / "unencrypted"
|
||||||
|
unencrypted_root_path.mkdir()
|
||||||
|
for i in range(5):
|
||||||
|
note = unencrypted_root_path / f"note_{i}.md"
|
||||||
|
note.touch()
|
||||||
|
subnote_path = unencrypted_root_path / f"inner_{i}"
|
||||||
|
subnote_path.mkdir()
|
||||||
|
for j in range(2):
|
||||||
|
subnote = subnote_path / f"note_{i}_{j}.md"
|
||||||
|
subnote.touch()
|
||||||
|
with subnote.open("w") as f:
|
||||||
|
f.write(f"subnote {i} {j}")
|
||||||
|
with note.open("w") as f:
|
||||||
|
f.write(f"note {i}")
|
||||||
|
return unencrypted_root_path
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def notebooks_command(settings: Settings):
|
def notebooks_command(settings: Settings):
|
||||||
|
|
|
||||||
36
tests/commands/test_import.py
Normal file
36
tests/commands/test_import.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from halig.commands.import_unencrypted import ImportCommand
|
||||||
|
from halig.encryption import Encryptor
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def command(settings) -> ImportCommand:
|
||||||
|
return ImportCommand(settings)
|
||||||
|
|
||||||
|
def test_get_importables(unencrypted_notes: Path, command: ImportCommand):
|
||||||
|
notes = list(command.get_importables())
|
||||||
|
assert len(notes) == 15
|
||||||
|
assert notes == list(unencrypted_notes.glob("**/*.md"))
|
||||||
|
|
||||||
|
def test_import(unencrypted_notes: Path, command: ImportCommand, encryptor: Encryptor):
|
||||||
|
command.run()
|
||||||
|
notes = command.get_importables()
|
||||||
|
encrypted_notes = list(unencrypted_notes.glob("**/*.age"))
|
||||||
|
assert len(encrypted_notes) == len(list(notes)) == 15
|
||||||
|
for note in encrypted_notes:
|
||||||
|
with note.open("rb") as f:
|
||||||
|
encrypted_data = f.read()
|
||||||
|
data = encryptor.decrypt(encrypted_data)
|
||||||
|
if "inner" in str(note):
|
||||||
|
assert f'sub{note.name.replace(".age", "").replace("_"," ")}' == data.decode()
|
||||||
|
else:
|
||||||
|
assert note.name.replace(".age", "").replace("_"," ") == data.decode()
|
||||||
|
|
||||||
|
def test_import_unlink(unencrypted_notes: Path, command: ImportCommand, encryptor: Encryptor):
|
||||||
|
command.unlink = True
|
||||||
|
command.run()
|
||||||
|
notes = command.get_importables()
|
||||||
|
encrypted_notes = list(unencrypted_notes.glob("**/*.age"))
|
||||||
|
assert len(encrypted_notes) == 15
|
||||||
|
assert len(list(unencrypted_notes.glob("**/*.md"))) == 0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue