Compare commits

..

1 commit

Author SHA1 Message Date
d628ce1c42
wip 2024-08-04 10:34:35 +02:00
40 changed files with 573 additions and 2044 deletions

View file

@ -1,73 +0,0 @@
---
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

View file

@ -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.6.4 rev: v0.4.4
hooks: hooks:
- id: ruff - id: ruff
args: args:

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.12.2

View file

@ -1,21 +1,21 @@
fmt: linters:
pre-commit run --all-files --color always pre-commit run --all-files --color always
.PHONY: tests .PHONY: tests
tests: tests:
pdm run pytest --cov=halig -vv tests --report-log reportlog.json rye run pytest --cov=halig -vv tests --report-log reportlog.json
pdm run coverage html rye run coverage html
pdm run coverage xml rye run coverage xml
build: build:
pdm build pdm build
publish-pypi: publish-pypi:
pdm publish -u $(PYPI_REGISTRY_USERNAME) -P $(PYPI_REGISTRY_PASSWORD) rye publish -u $(PYPI_REGISTRY_USERNAME) -P $(PYPI_REGISTRY_PASSWORD)
publish-roboces: publish-roboces:
pdm publish -u $(ROBOCES_REGISTRY_USERNAME) -P $(ROBOCES_REGISTRY_PASSWORD) -r https://git.roboces.dev/api/packages/catalin/pypi rye publish -u $(ROBOCES_REGISTRY_USERNAME) -P $(ROBOCES_REGISTRY_PASSWORD) -r https://git.roboces.dev/api/packages/catalin/pypi
publish: publish:
make publish-pypi make publish-pypi

View file

@ -1,6 +1,5 @@
# halig # halig
[![Build status](https://git.roboces.dev/catalin/halig/badges/workflows/ci.yaml/badge.svg)](https://git.roboces.dev/catalin/halig/actions)
![PyPI](https://img.shields.io/pypi/v/halig?logo=python) ![PyPI](https://img.shields.io/pypi/v/halig?logo=python)
![PyPI - License](https://img.shields.io/pypi/l/halig) ![PyPI - License](https://img.shields.io/pypi/l/halig)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/halig) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/halig)
@ -12,10 +11,11 @@
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
- Passphrase-less, fully-encrypted notes, compatible with existing SSH keys - Passphrase-less, fully-encrypted notes, compatible with existing SSH keys
- No external `age` binary needed - No external `age` binary needed
- Almost all `age` advantages, like having multiple keys for encryption and decryption - Almost all `age` advantages, like having multiple keys for encryption and decryption
- Remote (HTTP) public keys import: e.g: github.com/\<username\>.keys - Remote (HTTP) public keys import: e.g: github.com/\<username\>.keys
@ -35,7 +35,7 @@ mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/halig"
cat << EOF > "${XDG_CONFIG_HOME:-$HOME/.config}/halig/halig.yml" cat << EOF > "${XDG_CONFIG_HOME:-$HOME/.config}/halig/halig.yml"
--- ---
notebooks_root_path: ~/Documents/Notebooks notebooks_root_path: ~/Documents/Notebooks
identity_paths: identity_paths:
- ~/.ssh/id_ed25519 - ~/.ssh/id_ed25519
recipient_paths: recipient_paths:
- ~/.ssh/id_ed25519.pub - ~/.ssh/id_ed25519.pub
@ -50,6 +50,5 @@ 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
``` ```

View file

@ -1 +0,0 @@
__version__ = "0.6.2"

View file

@ -1,28 +0,0 @@
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)

View file

@ -1,14 +0,0 @@
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)

View file

@ -1,19 +0,0 @@
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")

View file

@ -1,16 +0,0 @@
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()

View file

@ -1,13 +0,0 @@
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

26
noxfile.py Normal file
View file

@ -0,0 +1,26 @@
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)

1589
pdm.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,21 @@
[project] [project]
authors = [
{ name = "cătălin", email = "185504a9@duck.com" },
]
requires-python = ">=3.10"
dependencies = [
"typer>=0",
"rich>=13.8",
"pydantic>=2.8",
"pyyaml>=6.0",
"pyrage>=1",
"httpx>=0",
"platformdirs>=4.2",
"pydantic-settings>=2",
"hishel>=0",
"whenever>=0.6",
"gitpython>=3.1",
]
name = "halig" name = "halig"
authors = [
{ name = "cătălin", email = "catalin@roboces.dev" }
]
dependencies = [
"typer>=0.12",
"rich>=13.3.3",
"pydantic>=2.7",
"pyyaml>=6.0",
"pyrage>=1.1",
"pendulum>=3.0",
"httpx>=0.27",
"platformdirs>=4.2",
"pydantic-settings>=2.0",
"hishel>=0.0.26",
]
requires-python = ">= 3.10"
dynamic = ["version"] dynamic = ["version"]
description = "age-encrypted, file-based, note-taking CLI app" description = "age-encrypted, file-based, note-taking CLI app"
readme = "README.md" readme = "README.md"
@ -41,39 +40,24 @@ Homepage = "https://git.roboces.dev/catalin/halig"
Repository = "https://git.roboces.dev/catalin/halig" Repository = "https://git.roboces.dev/catalin/halig"
Documentation = "https://git.roboces.dev/catalin/halig" Documentation = "https://git.roboces.dev/catalin/halig"
Changelog = "https://git.roboces.dev/catalin/halig" Changelog = "https://git.roboces.dev/catalin/halig"
[tool.pdm]
version = { source = "file", path = "halig/__version__.py" }
[tool.pdm.build]
excludes = ["**/.pytest_cache/**"]
includes = []
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project.scripts]
halig = "halig.main:app"
[project.optional-dependencies] [project.optional-dependencies]
testing = [ testing = [
"pytest>=7", "pytest>=7.2.2",
"pytest-cov>=4", "pytest-cov>=4.0.0",
"pyfakefs>=5", "pyfakefs>=5.1.0",
"pytest-clarity>=1", "pytest-clarity>=1.0.1",
"pytest-reportlog>=0", "pytest-reportlog>=0.2.1",
"pytest-duration-insights>=0", "pytest-duration-insights>=0.1.1",
"pytest-pretty>=1", "pytest-pretty>=1.1.1",
"pytest-mock>=3", "pytest-mock>=3.10.0",
"mock>=5", "mock>=5.0.1",
"faker>=28.1.0",
] ]
linting = [ linting = [
"ruff>=0.4", "black>=23.3.0",
"pyright>=1.1", "ruff>=0.1.0",
"mypy>=1.1", "pyright>=1.1.301",
"types-PyYAML>=6.0", "mypy>=1.1.1",
"types-PyYAML>=6.0.12.9",
] ]
docs = [ docs = [
"mkdocs-material>=9.1.5", "mkdocs-material>=9.1.5",
@ -82,6 +66,41 @@ docs = [
dev = [ dev = [
"bump-pydantic>=0.6.0", "bump-pydantic>=0.6.0",
] ]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.rye]
managed = true
dev-dependencies = []
[tool.hatch.build]
skip-excluded-dirs = true
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.version]
path = "src/halig/__version__.py"
[tool.hatch.build.targets.sdist]
exclude = [
"**/.pytest_cache/**",
"tests/**",
"noxfile.py",
"sample.env",
"Makefile",
".pre-commit-config.yaml",
".gitignore",
]
[tool.hatch.build.targets.wheel]
packages = ["src/halig"]
only-packages = true
[project.scripts]
halig = "halig.main:app"
[tool.pytest] [tool.pytest]
mock_use_standalone_module = true mock_use_standalone_module = true
@ -91,7 +110,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", "FURB", "PERF"] 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", "COM812", "ISC001"] extend-ignore = ["S101", "ISC002", "COM812", "ISC001"]
[tool.mypy] [tool.mypy]
@ -108,10 +127,3 @@ 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"
]

View file

@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

223
requirements-dev.lock Normal file
View file

@ -0,0 +1,223 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: true
# with-sources: false
-e file:.
annotated-types==0.7.0
# via pydantic
anyio==4.4.0
# via httpx
babel==2.15.0
# via mkdocs-material
black==24.8.0
# via halig
bump-pydantic==0.8.0
# via halig
certifi==2024.7.4
# via httpcore
# via httpx
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via black
# via mkdocs
# via mkdocstrings
# via typer
clumper==0.2.15
# via pytest-duration-insights
colorama==0.4.6
# via griffe
# via mkdocs-material
coverage==7.6.0
# via pytest-cov
ghp-import==2.1.0
# via mkdocs
griffe==0.48.0
# via mkdocstrings-python
h11==0.14.0
# via httpcore
hishel==0.0.30
# via halig
httpcore==1.0.5
# via httpx
httpx==0.27.0
# via halig
# via hishel
idna==3.7
# via anyio
# via httpx
# via requests
iniconfig==2.0.0
# via pytest
jinja2==3.1.4
# via mkdocs
# via mkdocs-material
# via mkdocstrings
libcst==1.4.0
# via bump-pydantic
markdown==3.6
# via mkdocs
# via mkdocs-autorefs
# via mkdocs-material
# via mkdocstrings
# via pymdown-extensions
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
# via jinja2
# via mkdocs
# via mkdocs-autorefs
# via mkdocstrings
mdurl==0.1.2
# via markdown-it-py
mergedeep==1.3.4
# via mkdocs
# via mkdocs-get-deps
mkdocs==1.6.0
# via mkdocs-autorefs
# via mkdocs-material
# via mkdocstrings
mkdocs-autorefs==1.0.1
# via mkdocstrings
mkdocs-get-deps==0.2.0
# via mkdocs
mkdocs-material==9.5.31
# via halig
mkdocs-material-extensions==1.3.1
# via mkdocs-material
mkdocstrings==0.25.2
# via halig
# via mkdocstrings-python
mkdocstrings-python==1.10.7
# via mkdocstrings
mock==5.1.0
# via halig
mypy==1.11.1
# via halig
mypy-extensions==1.0.0
# via black
# via mypy
nodeenv==1.9.1
# via pyright
packaging==24.1
# via black
# via mkdocs
# via pytest
paginate==0.5.6
# via mkdocs-material
parse==1.20.2
# via pytest-duration-insights
pathspec==0.12.1
# via black
# via mkdocs
pendulum==3.0.0
# via halig
platformdirs==4.2.2
# via black
# via halig
# via mkdocs-get-deps
# via mkdocstrings
pluggy==1.5.0
# via pytest
pprintpp==0.4.0
# via pytest-clarity
pydantic==2.8.2
# via halig
# via pydantic-settings
pydantic-core==2.20.1
# via pydantic
pydantic-settings==2.4.0
# via halig
pyfakefs==5.6.0
# via halig
pygments==2.18.0
# via mkdocs-material
# via rich
pymdown-extensions==10.9
# via mkdocs-material
# via mkdocstrings
pyrage==1.1.2
# via halig
pyright==1.1.374
# via halig
pytest==8.3.2
# via halig
# via pytest-clarity
# via pytest-cov
# via pytest-mock
# via pytest-pretty
# via pytest-reportlog
pytest-clarity==1.0.1
# via halig
pytest-cov==5.0.0
# via halig
pytest-duration-insights==0.1.2
# via halig
pytest-mock==3.14.0
# via halig
pytest-pretty==1.2.0
# via halig
pytest-reportlog==0.4.0
# via halig
# via pytest-duration-insights
python-dateutil==2.9.0.post0
# via ghp-import
# via pendulum
# via time-machine
python-dotenv==1.0.1
# via pydantic-settings
pyyaml==6.0.1
# via halig
# via libcst
# via mkdocs
# via mkdocs-get-deps
# via pymdown-extensions
# via pyyaml-env-tag
pyyaml-env-tag==0.1
# via mkdocs
regex==2024.7.24
# via mkdocs-material
requests==2.32.3
# via mkdocs-material
rich==13.7.1
# via bump-pydantic
# via halig
# via pytest-clarity
# via pytest-pretty
# via typer
ruff==0.5.6
# via halig
shellingham==1.5.4
# via typer
six==1.16.0
# via python-dateutil
sniffio==1.3.1
# via anyio
# via httpx
time-machine==2.14.2
# via pendulum
typer==0.12.3
# via bump-pydantic
# via halig
# via pytest-duration-insights
types-pyyaml==6.0.12.20240724
# via halig
typing-extensions==4.12.2
# via bump-pydantic
# via hishel
# via mypy
# via pydantic
# via pydantic-core
# via typer
tzdata==2024.1
# via pendulum
urllib3==2.2.2
# via requests
watchdog==4.0.1
# via mkdocs

223
requirements.lock Normal file
View file

@ -0,0 +1,223 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: true
# with-sources: false
-e file:.
annotated-types==0.7.0
# via pydantic
anyio==4.4.0
# via httpx
babel==2.15.0
# via mkdocs-material
black==24.8.0
# via halig
bump-pydantic==0.8.0
# via halig
certifi==2024.7.4
# via httpcore
# via httpx
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via black
# via mkdocs
# via mkdocstrings
# via typer
clumper==0.2.15
# via pytest-duration-insights
colorama==0.4.6
# via griffe
# via mkdocs-material
coverage==7.6.0
# via pytest-cov
ghp-import==2.1.0
# via mkdocs
griffe==0.48.0
# via mkdocstrings-python
h11==0.14.0
# via httpcore
hishel==0.0.30
# via halig
httpcore==1.0.5
# via httpx
httpx==0.27.0
# via halig
# via hishel
idna==3.7
# via anyio
# via httpx
# via requests
iniconfig==2.0.0
# via pytest
jinja2==3.1.4
# via mkdocs
# via mkdocs-material
# via mkdocstrings
libcst==1.4.0
# via bump-pydantic
markdown==3.6
# via mkdocs
# via mkdocs-autorefs
# via mkdocs-material
# via mkdocstrings
# via pymdown-extensions
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
# via jinja2
# via mkdocs
# via mkdocs-autorefs
# via mkdocstrings
mdurl==0.1.2
# via markdown-it-py
mergedeep==1.3.4
# via mkdocs
# via mkdocs-get-deps
mkdocs==1.6.0
# via mkdocs-autorefs
# via mkdocs-material
# via mkdocstrings
mkdocs-autorefs==1.0.1
# via mkdocstrings
mkdocs-get-deps==0.2.0
# via mkdocs
mkdocs-material==9.5.31
# via halig
mkdocs-material-extensions==1.3.1
# via mkdocs-material
mkdocstrings==0.25.2
# via halig
# via mkdocstrings-python
mkdocstrings-python==1.10.7
# via mkdocstrings
mock==5.1.0
# via halig
mypy==1.11.1
# via halig
mypy-extensions==1.0.0
# via black
# via mypy
nodeenv==1.9.1
# via pyright
packaging==24.1
# via black
# via mkdocs
# via pytest
paginate==0.5.6
# via mkdocs-material
parse==1.20.2
# via pytest-duration-insights
pathspec==0.12.1
# via black
# via mkdocs
pendulum==3.0.0
# via halig
platformdirs==4.2.2
# via black
# via halig
# via mkdocs-get-deps
# via mkdocstrings
pluggy==1.5.0
# via pytest
pprintpp==0.4.0
# via pytest-clarity
pydantic==2.8.2
# via halig
# via pydantic-settings
pydantic-core==2.20.1
# via pydantic
pydantic-settings==2.4.0
# via halig
pyfakefs==5.6.0
# via halig
pygments==2.18.0
# via mkdocs-material
# via rich
pymdown-extensions==10.9
# via mkdocs-material
# via mkdocstrings
pyrage==1.1.2
# via halig
pyright==1.1.374
# via halig
pytest==8.3.2
# via halig
# via pytest-clarity
# via pytest-cov
# via pytest-mock
# via pytest-pretty
# via pytest-reportlog
pytest-clarity==1.0.1
# via halig
pytest-cov==5.0.0
# via halig
pytest-duration-insights==0.1.2
# via halig
pytest-mock==3.14.0
# via halig
pytest-pretty==1.2.0
# via halig
pytest-reportlog==0.4.0
# via halig
# via pytest-duration-insights
python-dateutil==2.9.0.post0
# via ghp-import
# via pendulum
# via time-machine
python-dotenv==1.0.1
# via pydantic-settings
pyyaml==6.0.1
# via halig
# via libcst
# via mkdocs
# via mkdocs-get-deps
# via pymdown-extensions
# via pyyaml-env-tag
pyyaml-env-tag==0.1
# via mkdocs
regex==2024.7.24
# via mkdocs-material
requests==2.32.3
# via mkdocs-material
rich==13.7.1
# via bump-pydantic
# via halig
# via pytest-clarity
# via pytest-pretty
# via typer
ruff==0.5.6
# via halig
shellingham==1.5.4
# via typer
six==1.16.0
# via python-dateutil
sniffio==1.3.1
# via anyio
# via httpx
time-machine==2.14.2
# via pendulum
typer==0.12.3
# via bump-pydantic
# via halig
# via pytest-duration-insights
types-pyyaml==6.0.12.20240724
# via halig
typing-extensions==4.12.2
# via bump-pydantic
# via hishel
# via mypy
# via pydantic
# via pydantic-core
# via typer
tzdata==2024.1
# via pendulum
urllib3==2.2.2
# via requests
watchdog==4.0.1
# via mkdocs

1
src/halig/__version__.py Normal file
View file

@ -0,0 +1 @@
__version__ = "0.5.1a1"

View file

@ -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_as_date()}.age" self.note_path /= f"{utils.now().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"

View file

@ -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] # noqa: PERF203 except pyrage.DecryptError: # type: ignore[reportGeneralTypeIssues]
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 ...",

View file

@ -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_as_date()}.age" self.note_path /= f"{utils.now().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"

View file

@ -1,5 +1,3 @@
# 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"
@ -12,12 +10,6 @@ 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"

View file

@ -9,10 +9,6 @@ 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
@ -21,16 +17,7 @@ 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
git_app = Typer( app = Typer(pretty_exceptions_enable=False, pretty_exceptions_show_locals=False)
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)
@ -138,9 +125,8 @@ 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(config) settings = load_from_file()
command = SearchCommand( command = SearchCommand(
term=term, term=term,
index=index, index=index,
@ -150,53 +136,14 @@ def search(
@app.command(help=literals.COMMANDS_REENCRYPT_HELP) @app.command(help=literals.COMMANDS_REENCRYPT_HELP)
def reencrypt(config: Path | None = config_option): def reencrypt():
settings = load_from_file(config) settings = load_from_file()
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():

View file

@ -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,7 +47,6 @@ 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

View file

@ -2,16 +2,14 @@ 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():
return Instant.now() tz = local_timezone()
return pendulum.now(tz) # type: ignore[reportArgumentType]
def now_as_date():
return now().py_datetime().date()
def capture(fn: Callable): def capture(fn: Callable):

View file

@ -1,5 +1,6 @@
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
@ -23,10 +24,10 @@ def notes(notebooks_path: Path):
dailies = work / "Dailies" dailies = work / "Dailies"
dailies.mkdir() dailies.mkdir()
dt = utils.now() dt = pendulum.now()
for day_offset in range(10): for day_offset in range(10):
dt = dt.subtract(hours=day_offset*24) dt = dt.subtract(days=day_offset)
(dailies / f"{dt.py_datetime().date()}.age").touch() (dailies / f"{dt.date()}.age").touch()
@pytest.fixture() @pytest.fixture()
@ -55,7 +56,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_as_date()}.age" note_path = settings.notebooks_root_path / f"{utils.now().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:
@ -66,7 +67,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_as_date()}.age" settings.notebooks_root_path / "Work" / "Dailies" / f"{utils.now().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:

View file

@ -1,59 +0,0 @@
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]

View file

@ -1,37 +0,0 @@
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()

View file

@ -1,38 +0,0 @@
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()

View file

@ -71,14 +71,19 @@ 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(notebooks_root_path=notebooks_path,identity_paths=[halig_ssh_path / "id_ed25519"],recipient_paths=[halig_ssh_path / "id_ed25519.pub"]) return Settings(
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(settings, halig_config_path: Path, notebooks_path: Path) -> Path: def settings_file_path(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, identity_paths=settings.identity_paths, recipient_paths=settings.recipient_paths) s = Settings(notebooks_root_path=notebooks_path)
# `.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)

View file

@ -6,8 +6,7 @@ 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(recipient_paths=settings.recipient_paths, from_env_settings = Settings() # type: ignore[call-arg]
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
@ -30,10 +29,3 @@ 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]]