Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
586e891ad8 |
|||
|
7a1cdaa003 |
|||
|
d7bf338d96 |
|||
|
9dd2405c47 |
|||
|
4746e3b3b1 |
|||
|
8076807f22 |
|||
|
c859c60c8d |
|||
|
b1ed583a24 |
|||
|
4a6b3f5c0d |
|||
|
bdb5c984fa |
29 changed files with 1123 additions and 644 deletions
73
.forgejo/workflows/ci.yaml
Normal file
73
.forgejo/workflows/ci.yaml
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
name: checks
|
||||||
|
on: # yamllint disable-line rule:truthy
|
||||||
|
- 'push'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: https://code.forgejo.org/actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Setup PDM
|
||||||
|
uses: pdm-project/setup-pdm@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pdm install -G linting
|
||||||
|
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
|
|
||||||
|
tests-10:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: https://code.forgejo.org/actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Setup PDM
|
||||||
|
uses: pdm-project/setup-pdm@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pdm install -G testing
|
||||||
|
|
||||||
|
- run: make tests
|
||||||
|
|
||||||
|
tests-11:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: https://code.forgejo.org/actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Setup PDM
|
||||||
|
uses: pdm-project/setup-pdm@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pdm install -G testing
|
||||||
|
|
||||||
|
- run: make tests
|
||||||
|
|
||||||
|
tests-12:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: https://code.forgejo.org/actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Setup PDM
|
||||||
|
uses: pdm-project/setup-pdm@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pdm install -G testing
|
||||||
|
|
||||||
|
- run: make tests
|
||||||
|
|
@ -19,7 +19,7 @@ repos:
|
||||||
args: [ --fix=lf ]
|
args: [ --fix=lf ]
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
rev: v0.4.4
|
rev: v0.6.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
linters:
|
fmt:
|
||||||
pre-commit run --all-files --color always
|
pre-commit run --all-files --color always
|
||||||
|
|
||||||
.PHONY: tests
|
.PHONY: tests
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# halig
|
# halig
|
||||||
|
|
||||||
|
[](https://git.roboces.dev/catalin/halig/actions)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
@ -11,7 +12,6 @@
|
||||||
it encrypts the new contents into an [age](https://github.com/FiloSottile/age) file that
|
it encrypts the new contents into an [age](https://github.com/FiloSottile/age) file that
|
||||||
you can store, _relatively_ safe, anywhere.
|
you can store, _relatively_ safe, anywhere.
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Simple notebooks management with paths autocompletion
|
- Simple notebooks management with paths autocompletion
|
||||||
|
|
@ -50,5 +50,6 @@ EOF
|
||||||
halig edit some_notebook # edit today's note relative to <notebooks_root_path>/some_notebook
|
halig edit some_notebook # edit today's note relative to <notebooks_root_path>/some_notebook
|
||||||
halig edit some_notebook/foo # edit <notebooks_root_path>/some_notebook/foo.age
|
halig edit some_notebook/foo # edit <notebooks_root_path>/some_notebook/foo.age
|
||||||
halig notebooks # list current notebooks
|
halig notebooks # list current notebooks
|
||||||
|
halig git commit
|
||||||
|
halig git push
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.5.0"
|
__version__ = "0.6.2"
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class EditCommand(BaseCommand):
|
||||||
self.encryptor = Encryptor(self.settings)
|
self.encryptor = Encryptor(self.settings)
|
||||||
|
|
||||||
if self.note_path.is_dir():
|
if self.note_path.is_dir():
|
||||||
self.note_path /= f"{utils.now().date()}.age"
|
self.note_path /= f"{utils.now_as_date()}.age"
|
||||||
|
|
||||||
if not self.note_path.name.endswith(".age"):
|
if not self.note_path.name.endswith(".age"):
|
||||||
err = f"File {self.note_path.name} is not a valid AGE file"
|
err = f"File {self.note_path.name} is not a valid AGE file"
|
||||||
|
|
|
||||||
0
halig/commands/git/__init__.py
Normal file
0
halig/commands/git/__init__.py
Normal file
28
halig/commands/git/base.py
Normal file
28
halig/commands/git/base.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from git import Repo
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from halig.commands.base import BaseCommand
|
||||||
|
from halig.encryption import Encryptor
|
||||||
|
from halig.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class GitBaseCommand(BaseCommand):
|
||||||
|
@staticmethod
|
||||||
|
def __init_repo(repo_path: Path) -> Repo:
|
||||||
|
"""Check if `repo_path` is a git repo. If not, initialize it"""
|
||||||
|
|
||||||
|
if not (repo_path / ".git").is_dir():
|
||||||
|
print(f"[yellow] {repo_path} is not a git repo, initializing ...")
|
||||||
|
Repo.init(repo_path)
|
||||||
|
return Repo(repo_path)
|
||||||
|
|
||||||
|
return Repo(repo_path)
|
||||||
|
|
||||||
|
def __init__(self, settings: Settings, message: str | None = None):
|
||||||
|
super().__init__(settings)
|
||||||
|
self.settings = settings
|
||||||
|
self.encryptor = Encryptor(self.settings)
|
||||||
|
self.message = message or self.settings.default_commit_message
|
||||||
|
self.repo = self.__init_repo(self.settings.notebooks_root_path)
|
||||||
14
halig/commands/git/commit.py
Normal file
14
halig/commands/git/commit.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
from halig.commands.git.base import GitBaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class GitCommitCommand(GitBaseCommand):
|
||||||
|
def __init__(self, message: str | None = None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.message = message or self.settings.default_commit_message
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Add all .age files to git and commit them using gitpython"""
|
||||||
|
self.repo.index.add(
|
||||||
|
[str(path) for path in self.settings.notebooks_root_path.glob("**/*.age")]
|
||||||
|
)
|
||||||
|
self.repo.index.commit(self.message)
|
||||||
19
halig/commands/git/pull.py
Normal file
19
halig/commands/git/pull.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from halig.commands.git.base import GitBaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class GitPullCommand(GitBaseCommand):
|
||||||
|
def __init__(
|
||||||
|
self, remotes: list[str] | None = None, ref: str | None = None, *args, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.remotes = remotes
|
||||||
|
self.ref = ref
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Pull all changes from the remote git repo"""
|
||||||
|
if not self.remotes:
|
||||||
|
self.repo.remotes.origin.pull(self.ref or "main")
|
||||||
|
return
|
||||||
|
|
||||||
|
for remote in self.remotes:
|
||||||
|
self.repo.remotes[remote].pull(self.ref or "main")
|
||||||
16
halig/commands/git/push.py
Normal file
16
halig/commands/git/push.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from halig.commands.git.base import GitBaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class GitPushCommand(GitBaseCommand):
|
||||||
|
def __init__(self, remotes: list[str] | None = None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.remotes = remotes
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Push all changes to the remote git repo"""
|
||||||
|
if not self.remotes:
|
||||||
|
self.repo.remotes.origin.push()
|
||||||
|
return
|
||||||
|
|
||||||
|
for remote in self.remotes:
|
||||||
|
self.repo.remotes[remote].push()
|
||||||
13
halig/commands/git/status.py
Normal file
13
halig/commands/git/status.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from halig.commands.git.base import GitBaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class GitStatusCommand(GitBaseCommand):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs) # pragma: no cover
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Show the status of the git repo, including unstaged *.age files"""
|
||||||
|
|
||||||
|
print(self.repo.git.status()) # pragma: no cover
|
||||||
|
|
@ -19,7 +19,7 @@ class ReencryptCommand(BaseCommand):
|
||||||
new_data = self.encryptor.encrypt(orig_data)
|
new_data = self.encryptor.encrypt(orig_data)
|
||||||
with note_path.open("wb") as fw:
|
with note_path.open("wb") as fw:
|
||||||
fw.write(new_data)
|
fw.write(new_data)
|
||||||
except pyrage.DecryptError: # type: ignore[reportGeneralTypeIssues]
|
except pyrage.DecryptError: # type: ignore[reportGeneralTypeIssues] # noqa: PERF203
|
||||||
print(
|
print(
|
||||||
f"[yellow] Could not reencrypt {note_path} because no matching keys"
|
f"[yellow] Could not reencrypt {note_path} because no matching keys"
|
||||||
f" were found, skipping ...",
|
f" were found, skipping ...",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class ShowCommand(BaseCommand):
|
||||||
self.plain = plain
|
self.plain = plain
|
||||||
self.console = Console()
|
self.console = Console()
|
||||||
if self.note_path.is_dir():
|
if self.note_path.is_dir():
|
||||||
self.note_path /= f"{utils.now().date()}.age"
|
self.note_path /= f"{utils.now_as_date()}.age"
|
||||||
|
|
||||||
if not self.note_path.exists():
|
if not self.note_path.exists():
|
||||||
err = f"File {self.note_path.name} does not exist"
|
err = f"File {self.note_path.name} does not exist"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# APPS
|
||||||
|
GIT_HELP = "Git integration"
|
||||||
# COMMANDS
|
# COMMANDS
|
||||||
COMMANDS_NOTEBOOKS_HELP = "List all notebooks and notes, tree-style"
|
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"
|
||||||
|
|
@ -10,6 +12,12 @@ which are indexed into a SQLite FTS5 database located at `~/.cache/halig/halig.d
|
||||||
COMMANDS_REENCRYPT_HELP = """Reencrypt all available notes. This operation is useful
|
COMMANDS_REENCRYPT_HELP = """Reencrypt all available notes. This operation is useful
|
||||||
when new public keys have been added to the config file and you want the notes
|
when new public keys have been added to the config file and you want the notes
|
||||||
to be seen by the new pairing private keys"""
|
to be seen by the new pairing private keys"""
|
||||||
|
COMMANDS_GIT_COMMIT_HELP = "Commit all .age files to git"
|
||||||
|
COMMANDS_GIT_PUSH_HELP = "Push all .age files to git"
|
||||||
|
COMMANDS_GIT_PULL_HELP = "Pull all .age files from git"
|
||||||
|
COMMANDS_GIT_STATUS_HELP = (
|
||||||
|
"Show the status of the git repo, including unstaged *.age 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"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ 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.git.commit import GitCommitCommand
|
||||||
|
from halig.commands.git.pull import GitPullCommand
|
||||||
|
from halig.commands.git.push import GitPushCommand
|
||||||
|
from halig.commands.git.status import GitStatusCommand
|
||||||
from halig.commands.import_unencrypted import ImportCommand
|
from halig.commands.import_unencrypted import ImportCommand
|
||||||
from halig.commands.notebooks import NotebooksCommand
|
from halig.commands.notebooks import NotebooksCommand
|
||||||
from halig.commands.reencrypt import ReencryptCommand
|
from halig.commands.reencrypt import ReencryptCommand
|
||||||
|
|
@ -17,7 +21,16 @@ from halig.commands.show import ShowCommand
|
||||||
from halig.settings import load_from_file
|
from halig.settings import load_from_file
|
||||||
from halig.utils import capture
|
from halig.utils import capture
|
||||||
|
|
||||||
app = Typer(pretty_exceptions_enable=False, pretty_exceptions_show_locals=False)
|
git_app = Typer(
|
||||||
|
name="git",
|
||||||
|
help=literals.GIT_HELP,
|
||||||
|
pretty_exceptions_enable=False,
|
||||||
|
pretty_exceptions_show_locals=False,
|
||||||
|
)
|
||||||
|
app = Typer(
|
||||||
|
name="halig", pretty_exceptions_enable=False, pretty_exceptions_show_locals=False
|
||||||
|
)
|
||||||
|
app.add_typer(git_app)
|
||||||
|
|
||||||
config_option = Option(None, "--config", "-c", help=literals.OPTION_CONFIG_HELP)
|
config_option = Option(None, "--config", "-c", help=literals.OPTION_CONFIG_HELP)
|
||||||
|
|
||||||
|
|
@ -125,8 +138,9 @@ def import_unencrypted(
|
||||||
def search(
|
def search(
|
||||||
term: str,
|
term: str,
|
||||||
index: bool = Option(False, help=literals.OPTION_INDEX_HELP),
|
index: bool = Option(False, help=literals.OPTION_INDEX_HELP),
|
||||||
|
config: Optional[Path] = config_option, # noqa: UP007
|
||||||
):
|
):
|
||||||
settings = load_from_file()
|
settings = load_from_file(config)
|
||||||
command = SearchCommand(
|
command = SearchCommand(
|
||||||
term=term,
|
term=term,
|
||||||
index=index,
|
index=index,
|
||||||
|
|
@ -136,14 +150,53 @@ def search(
|
||||||
|
|
||||||
|
|
||||||
@app.command(help=literals.COMMANDS_REENCRYPT_HELP)
|
@app.command(help=literals.COMMANDS_REENCRYPT_HELP)
|
||||||
def reencrypt():
|
def reencrypt(config: Path | None = config_option):
|
||||||
settings = load_from_file()
|
settings = load_from_file(config)
|
||||||
command = ReencryptCommand(
|
command = ReencryptCommand(
|
||||||
settings=settings,
|
settings=settings,
|
||||||
)
|
)
|
||||||
command.run()
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
|
@git_app.command(name="commit", help=literals.COMMANDS_GIT_COMMIT_HELP)
|
||||||
|
def git_commit(
|
||||||
|
message: str | None = None,
|
||||||
|
config: Path | None = config_option,
|
||||||
|
):
|
||||||
|
settings = load_from_file(config)
|
||||||
|
command = GitCommitCommand(settings=settings, message=message)
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
|
@git_app.command(name="push", help=literals.COMMANDS_GIT_PUSH_HELP)
|
||||||
|
def git_push(
|
||||||
|
remotes: list[str] | None = None,
|
||||||
|
config: Path | None = config_option,
|
||||||
|
):
|
||||||
|
settings = load_from_file(config)
|
||||||
|
command = GitPushCommand(settings=settings, remotes=remotes)
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
|
@git_app.command(name="pull", help=literals.COMMANDS_GIT_PULL_HELP)
|
||||||
|
def git_pull(
|
||||||
|
remotes: list[str] | None = None,
|
||||||
|
config: Path | None = config_option,
|
||||||
|
):
|
||||||
|
settings = load_from_file(config)
|
||||||
|
command = GitPullCommand(settings=settings, remotes=remotes)
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
|
@git_app.command(name="status", help=literals.COMMANDS_GIT_STATUS_HELP)
|
||||||
|
def git_status(
|
||||||
|
config: Path | None = config_option,
|
||||||
|
):
|
||||||
|
settings = load_from_file(config)
|
||||||
|
command = GitStatusCommand(settings=settings)
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
@app.command(help=literals.COMMANDS_VERSION)
|
@app.command(help=literals.COMMANDS_VERSION)
|
||||||
@capture
|
@capture
|
||||||
def version():
|
def version():
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class Settings(BaseSettings):
|
||||||
cache_path (DirectoryPath): a *valid* path used to cache some stuff,
|
cache_path (DirectoryPath): a *valid* path used to cache some stuff,
|
||||||
particularly remote public keys. Defaults to $XDG_CACHE_HOME/halig
|
particularly remote public keys. Defaults to $XDG_CACHE_HOME/halig
|
||||||
remote_public_keys_timeout (float): time after which the retrieval of external public keys
|
remote_public_keys_timeout (float): time after which the retrieval of external public keys
|
||||||
(e.g. github ssh keys) should be interrupted. Defaults to 0.5.
|
(e.g. GitHub ssh keys) should be interrupted. Defaults to 0.5.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
notebooks_root_path: DirectoryPath
|
notebooks_root_path: DirectoryPath
|
||||||
|
|
@ -47,6 +47,7 @@ class Settings(BaseSettings):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
remote_public_keys_timeout: float = 0.5
|
remote_public_keys_timeout: float = 0.5
|
||||||
|
default_commit_message: str = "Update notebooks"
|
||||||
|
|
||||||
@field_validator("identity_paths", "recipient_paths", mode="before")
|
@field_validator("identity_paths", "recipient_paths", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,16 @@ import sys
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import pendulum
|
|
||||||
from pendulum.tz import local_timezone
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
from whenever import Instant
|
||||||
|
|
||||||
|
|
||||||
def now():
|
def now():
|
||||||
tz = local_timezone()
|
return Instant.now()
|
||||||
return pendulum.now(tz) # type: ignore[reportArgumentType]
|
|
||||||
|
|
||||||
|
def now_as_date():
|
||||||
|
return now().py_datetime().date()
|
||||||
|
|
||||||
|
|
||||||
def capture(fn: Callable):
|
def capture(fn: Callable):
|
||||||
|
|
|
||||||
26
noxfile.py
26
noxfile.py
|
|
@ -1,26 +0,0 @@
|
||||||
import nox
|
|
||||||
|
|
||||||
VERSIONS = ["3.10", "3.11", "3.12"]
|
|
||||||
|
|
||||||
|
|
||||||
@nox.session(python=VERSIONS)
|
|
||||||
def tests(session):
|
|
||||||
session.run(
|
|
||||||
"pdm",
|
|
||||||
"export",
|
|
||||||
"-G",
|
|
||||||
"testing",
|
|
||||||
"-f",
|
|
||||||
"requirements",
|
|
||||||
"-o",
|
|
||||||
"requirements.txt",
|
|
||||||
external=True,
|
|
||||||
)
|
|
||||||
session.install("-r", "requirements.txt")
|
|
||||||
session.run("make", "tests", external=True)
|
|
||||||
session.run("rm", "requirements.txt", external=True)
|
|
||||||
|
|
||||||
|
|
||||||
@nox.session(python=VERSIONS)
|
|
||||||
def linters(session):
|
|
||||||
session.run("make", "linters", external=True)
|
|
||||||
|
|
@ -4,16 +4,17 @@ authors = [
|
||||||
]
|
]
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typer>=0.12",
|
"typer>=0",
|
||||||
"rich>=13.3.3",
|
"rich>=13.8",
|
||||||
"pydantic>=2.7",
|
"pydantic>=2.8",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"pyrage>=1.1",
|
"pyrage>=1",
|
||||||
"pendulum>=3.0",
|
"httpx>=0",
|
||||||
"httpx>=0.27",
|
|
||||||
"platformdirs>=4.2",
|
"platformdirs>=4.2",
|
||||||
"pydantic-settings>=2.0",
|
"pydantic-settings>=2",
|
||||||
"hishel>=0.0.26",
|
"hishel>=0",
|
||||||
|
"whenever>=0.6",
|
||||||
|
"gitpython>=3.1",
|
||||||
]
|
]
|
||||||
name = "halig"
|
name = "halig"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
@ -57,22 +58,22 @@ halig = "halig.main:app"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
testing = [
|
testing = [
|
||||||
"pytest>=7.2.2",
|
"pytest>=7",
|
||||||
"pytest-cov>=4.0.0",
|
"pytest-cov>=4",
|
||||||
"pyfakefs>=5.1.0",
|
"pyfakefs>=5",
|
||||||
"pytest-clarity>=1.0.1",
|
"pytest-clarity>=1",
|
||||||
"pytest-reportlog>=0.2.1",
|
"pytest-reportlog>=0",
|
||||||
"pytest-duration-insights>=0.1.1",
|
"pytest-duration-insights>=0",
|
||||||
"pytest-pretty>=1.1.1",
|
"pytest-pretty>=1",
|
||||||
"pytest-mock>=3.10.0",
|
"pytest-mock>=3",
|
||||||
"mock>=5.0.1",
|
"mock>=5",
|
||||||
|
"faker>=28.1.0",
|
||||||
]
|
]
|
||||||
linting = [
|
linting = [
|
||||||
"black>=23.3.0",
|
"ruff>=0.4",
|
||||||
"ruff>=0.1.0",
|
"pyright>=1.1",
|
||||||
"pyright>=1.1.301",
|
"mypy>=1.1",
|
||||||
"mypy>=1.1.1",
|
"types-PyYAML>=6.0",
|
||||||
"types-PyYAML>=6.0.12.9",
|
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"mkdocs-material>=9.1.5",
|
"mkdocs-material>=9.1.5",
|
||||||
|
|
@ -90,7 +91,7 @@ reportMissingTypeStubs = false
|
||||||
reportAttributeAccessIssue = false
|
reportAttributeAccessIssue = false
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
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-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", "FURB", "PERF"]
|
||||||
extend-ignore = ["S101", "ISC002", "COM812", "ISC001"]
|
extend-ignore = ["S101", "ISC002", "COM812", "ISC001"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
|
@ -107,3 +108,10 @@ module = [
|
||||||
"httpx_cache"
|
"httpx_cache"
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
omit = [
|
||||||
|
"halig/__version__.py",
|
||||||
|
"halig/literals.py",
|
||||||
|
"halig/main.py"
|
||||||
|
]
|
||||||
3
renovate.json
Normal file
3
renovate.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pendulum
|
|
||||||
import pytest as pytest
|
import pytest as pytest
|
||||||
|
|
||||||
from halig import utils
|
from halig import utils
|
||||||
|
|
@ -24,10 +23,10 @@ def notes(notebooks_path: Path):
|
||||||
dailies = work / "Dailies"
|
dailies = work / "Dailies"
|
||||||
dailies.mkdir()
|
dailies.mkdir()
|
||||||
|
|
||||||
dt = pendulum.now()
|
dt = utils.now()
|
||||||
for day_offset in range(10):
|
for day_offset in range(10):
|
||||||
dt = dt.subtract(days=day_offset)
|
dt = dt.subtract(hours=day_offset*24)
|
||||||
(dailies / f"{dt.date()}.age").touch()
|
(dailies / f"{dt.py_datetime().date()}.age").touch()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|
@ -56,7 +55,7 @@ def notebooks_command(settings: Settings):
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def current_note(notes, settings, encryptor) -> Path:
|
def current_note(notes, settings, encryptor) -> Path:
|
||||||
note_path = settings.notebooks_root_path / f"{utils.now().date()}.age"
|
note_path = settings.notebooks_root_path / f"{utils.now_as_date()}.age"
|
||||||
note_path.touch()
|
note_path.touch()
|
||||||
data = encryptor.encrypt(b"foo")
|
data = encryptor.encrypt(b"foo")
|
||||||
with note_path.open("wb") as f:
|
with note_path.open("wb") as f:
|
||||||
|
|
@ -67,7 +66,7 @@ def current_note(notes, settings, encryptor) -> Path:
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def current_daily(notes, settings, encryptor) -> Path:
|
def current_daily(notes, settings, encryptor) -> Path:
|
||||||
note_path = (
|
note_path = (
|
||||||
settings.notebooks_root_path / "Work" / "Dailies" / f"{utils.now().date()}.age"
|
settings.notebooks_root_path / "Work" / "Dailies" / f"{utils.now_as_date()}.age"
|
||||||
)
|
)
|
||||||
data = encryptor.encrypt(b"foo")
|
data = encryptor.encrypt(b"foo")
|
||||||
with note_path.open("wb") as f:
|
with note_path.open("wb") as f:
|
||||||
|
|
|
||||||
0
tests/commands/test_git/__init__.py
Normal file
0
tests/commands/test_git/__init__.py
Normal file
59
tests/commands/test_git/test_commit.py
Normal file
59
tests/commands/test_git/test_commit.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from halig.commands.git.commit import GitCommitCommand
|
||||||
|
from halig.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def command(settings: Settings):
|
||||||
|
return GitCommitCommand(settings=settings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_is_not_initialized(settings):
|
||||||
|
"""Given that settings.notebooks_root_path is not a git repo, assert that the command
|
||||||
|
initializes the repo upon instantiation"""
|
||||||
|
|
||||||
|
assert not (settings.notebooks_root_path / ".git").is_dir()
|
||||||
|
GitCommitCommand(settings=settings)
|
||||||
|
assert (settings.notebooks_root_path / ".git").is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_is_initialized(settings):
|
||||||
|
"""Manually initialize a repo in settings.notebooks_root_path and check that the command instantiation
|
||||||
|
is not reinitializing it"""
|
||||||
|
|
||||||
|
p = subprocess.Popen(["git", "init"], cwd=settings.notebooks_root_path)
|
||||||
|
p.wait()
|
||||||
|
assert (settings.notebooks_root_path / ".git").is_dir()
|
||||||
|
GitCommitCommand(settings=settings)
|
||||||
|
assert (settings.notebooks_root_path / ".git").is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_run(settings, command, faker):
|
||||||
|
"""Create a bunch of .age and non-.age files and assert that all .age files are added to git and that the commit
|
||||||
|
message is set"""
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
random_file = settings.notebooks_root_path / f"{faker.word()}.txt"
|
||||||
|
random_file.touch()
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
random_age_file = settings.notebooks_root_path / f"{faker.word()}.age"
|
||||||
|
random_age_file.touch()
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
assert settings.notebooks_root_path / ".git" / "index"
|
||||||
|
assert settings.notebooks_root_path / ".git" / "index" / "stage"
|
||||||
|
|
||||||
|
assert command.message in command.repo.git.log("--pretty=oneline").splitlines()[0]
|
||||||
|
|
||||||
|
assert "nothing added to commit but untracked files present (use \"git add\" to track)" in command.repo.git.status()
|
||||||
|
assert ".age" not in command.repo.git.status()
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_commit_message(settings, command, faker):
|
||||||
|
command.message = faker.word()
|
||||||
|
command.run()
|
||||||
|
assert command.message in command.repo.git.log("--pretty=oneline").splitlines()[0]
|
||||||
37
tests/commands/test_git/test_pull.py
Normal file
37
tests/commands/test_git/test_pull.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from git import Repo
|
||||||
|
|
||||||
|
from halig.commands.git.pull import GitPullCommand
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def command(settings, faker):
|
||||||
|
"""Configure a local remote for testing located at settings.notebooks_root_path/../remote, push some .age files to
|
||||||
|
that remote
|
||||||
|
"""
|
||||||
|
command = GitPullCommand(settings=settings)
|
||||||
|
|
||||||
|
new_path = shutil.copytree(settings.notebooks_root_path, settings.notebooks_root_path / "../remote")
|
||||||
|
new_path = new_path.resolve()
|
||||||
|
|
||||||
|
command.repo.create_remote("origin", str(new_path))
|
||||||
|
|
||||||
|
remote_repo = Repo(new_path)
|
||||||
|
for _ in range(10):
|
||||||
|
random_age_file = new_path / f"{faker.word()}.age"
|
||||||
|
random_age_file.touch()
|
||||||
|
remote_repo.index.add([str(random_age_file)])
|
||||||
|
remote_repo.index.commit("Update notebooks")
|
||||||
|
|
||||||
|
return command
|
||||||
|
|
||||||
|
def test_pull_from_origin(command):
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
def test_pull_from_custom_origin(settings, command):
|
||||||
|
remote_path = settings.notebooks_root_path / "../remote"
|
||||||
|
command.repo.create_remote("custom", str(remote_path.resolve()))
|
||||||
|
command.remotes = ["custom"]
|
||||||
|
command.run()
|
||||||
38
tests/commands/test_git/test_push.py
Normal file
38
tests/commands/test_git/test_push.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from halig.commands.git.commit import GitCommitCommand
|
||||||
|
from halig.commands.git.push import GitPushCommand
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def command(settings, faker):
|
||||||
|
"""Configure a local remote for testing"""
|
||||||
|
commit_command = GitCommitCommand(settings=settings)
|
||||||
|
new_path = shutil.copytree(settings.notebooks_root_path, settings.notebooks_root_path / "../remote")
|
||||||
|
new_path = new_path.resolve()
|
||||||
|
for _ in range(10):
|
||||||
|
random_age_file = settings.notebooks_root_path / f"{faker.word()}.age"
|
||||||
|
random_age_file.touch()
|
||||||
|
commit_command.run()
|
||||||
|
|
||||||
|
push_command = GitPushCommand(settings=settings)
|
||||||
|
|
||||||
|
push_command.repo.create_remote("origin", str(new_path))
|
||||||
|
|
||||||
|
return push_command
|
||||||
|
|
||||||
|
|
||||||
|
def test_push_to_origin(command):
|
||||||
|
"""Test that the command pushes to the origin remote"""
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
|
||||||
|
def test_push_to_custom_remote(settings, command):
|
||||||
|
"""Test that the command pushes to a custom remote"""
|
||||||
|
|
||||||
|
remote_path = settings.notebooks_root_path / "../remote"
|
||||||
|
command.repo.create_remote("custom", str(remote_path.resolve()))
|
||||||
|
command.remotes = ["custom"]
|
||||||
|
command.run()
|
||||||
|
|
@ -71,19 +71,14 @@ def notebooks_path(tmp_path) -> Path:
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def settings(notebooks_path: Path, halig_ssh_path) -> Settings:
|
def settings(notebooks_path: Path, halig_ssh_path) -> Settings:
|
||||||
return Settings(
|
return Settings(notebooks_root_path=notebooks_path,identity_paths=[halig_ssh_path / "id_ed25519"],recipient_paths=[halig_ssh_path / "id_ed25519.pub"])
|
||||||
notebooks_root_path=notebooks_path,
|
|
||||||
identity_paths=[halig_ssh_path / "id_ed25519"],
|
|
||||||
recipient_paths=[halig_ssh_path / "id_ed25519.pub"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def settings_file_path(halig_config_path: Path, notebooks_path: Path) -> Path:
|
def settings_file_path(settings, halig_config_path: Path, notebooks_path: Path) -> Path:
|
||||||
yaml_file = halig_config_path / "halig.yml"
|
yaml_file = halig_config_path / "halig.yml"
|
||||||
yaml_file.touch()
|
yaml_file.touch()
|
||||||
s = Settings(notebooks_root_path=notebooks_path)
|
s = Settings(notebooks_root_path=notebooks_path, identity_paths=settings.identity_paths, recipient_paths=settings.recipient_paths)
|
||||||
# `.dict()` doesn't serialize some fields that yaml doesn't understand
|
|
||||||
serialized = json.loads(s.model_dump_json())
|
serialized = json.loads(s.model_dump_json())
|
||||||
with yaml_file.open("w") as f:
|
with yaml_file.open("w") as f:
|
||||||
yaml.safe_dump(serialized, f)
|
yaml.safe_dump(serialized, f)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ from halig.settings import Settings, load_from_file
|
||||||
|
|
||||||
|
|
||||||
def test_settings_from_env(settings: Settings, notebooks_root_path_envvar):
|
def test_settings_from_env(settings: Settings, notebooks_root_path_envvar):
|
||||||
from_env_settings = Settings() # type: ignore[call-arg]
|
from_env_settings = Settings(recipient_paths=settings.recipient_paths,
|
||||||
|
identity_paths=settings.identity_paths) # type: ignore[call-arg]
|
||||||
assert from_env_settings.notebooks_root_path == settings.notebooks_root_path
|
assert from_env_settings.notebooks_root_path == settings.notebooks_root_path
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,3 +30,10 @@ def test_load_from_non_existing_file_path_raises_file_not_found_error(halig_conf
|
||||||
file = halig_config_path / "some_invalid_file.yml"
|
file = halig_config_path / "some_invalid_file.yml"
|
||||||
with pytest.raises(FileNotFoundError, match=f"File {file} does not exist"):
|
with pytest.raises(FileNotFoundError, match=f"File {file} does not exist"):
|
||||||
load_from_file(file)
|
load_from_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_identity_paths_is_not_list_is_converted(settings):
|
||||||
|
s = Settings(identity_paths=settings.identity_paths[0], recipient_paths=settings.recipient_paths[0],
|
||||||
|
notebooks_root_path=settings.notebooks_root_path)
|
||||||
|
assert s.identity_paths == [settings.identity_paths[0]]
|
||||||
|
assert s.recipient_paths == [settings.recipient_paths[0]]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue