feat: add import command

This commit is contained in:
cătălin 2023-05-17 19:00:42 +02:00
commit be20284f78
Signed by: catalin
GPG key ID: 0178DF42F43E5FD2
9 changed files with 133 additions and 10 deletions

View file

@ -1 +1 @@
__version__ = "0.3.1" __version__ = "0.3.2"

View 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()

View file

@ -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

View file

@ -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():

View file

@ -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
View file

@ -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"},

View file

@ -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]

View file

@ -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):

View 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