Merge pull request 'Passwords template redesigned' (#12) from passwd into develop

Reviewed-on: https://hattori.ztsh.eu/iac/karl/pulls/12
This commit is contained in:
Piotr Dec 2025-11-26 00:07:20 +01:00
commit f1f1772c9a
5 changed files with 83 additions and 41 deletions

View file

@ -7,13 +7,9 @@ from injectable import injectable, autowired, Autowired
from app.services import Passwords
class ComplexValueTemplate(Template):
# Pozwala na kropki w nazwach placeholderów, np. ${user.name.first}
idpattern = r'[_a-zA-Z][_a-zA-Z0-9.]*'
class SimpleValueTemplate(ComplexValueTemplate):
delimiter = '%'
class ValueTemplate(Template):
# Pozwala na kropki i ukośniki w nazwach placeholderów, np. ${user.name/first}
idpattern = r'[_a-zA-Z][_a-zA-Z0-9.\/]*'
@injectable
@ -26,12 +22,9 @@ class Mo:
raw = ''
with open(mo_file, "r") as mo:
raw = mo.read()
cmp = ComplexValueTemplate(raw)
rendered = cmp.substitute(self._passwords.get_values(cmp.get_identifiers()))
smp = SimpleValueTemplate(rendered)
ids = [_id + '.password' for _id in smp.get_identifiers()]
mappings = {k.replace('.password', ''): v for k, v in self._passwords.get_values(ids).items()}
rendered = smp.substitute(mappings)
parsed = ValueTemplate(raw)
mappings = self._passwords.get_values(parsed.get_identifiers())
rendered = parsed.safe_substitute(mappings)
de_mo_ified = str(mo_file).replace(".mo", "")
with open(de_mo_ified, "w") as mo:
mo.write(rendered)

View file

@ -6,6 +6,30 @@ import keyring
from injectable import injectable
from pykeepass import PyKeePass, create_database
class KeyRequest:
def __init__(self, prompt: str):
self.field_name = None
self.entry_name = None
self.path = None
self._parse_prompt(prompt)
def _parse_prompt(self, prompt: str):
prompt_parts = prompt.split("/")
key = None
match len(prompt_parts):
case 1:
self.field_name = 'password'
key = prompt_parts[0]
case 2:
self.field_name = prompt_parts[1]
key = prompt_parts[0]
case _:
key = None
if key is None:
return
key_parts = key.split(".")
self.path = key_parts[:] if len(key_parts) > 1 else None
self.entry_name = key_parts[-1]
@injectable(singleton=True)
class Passwords:
@ -28,13 +52,10 @@ class Passwords:
def get_values(self, keys: list[str]) -> dict[str, str]:
output = {}
for k in keys:
key_parts = k.split(".")
path = key_parts[:-1] if len(key_parts) > 2 else None
entry_name = key_parts[-2]
field_name = key_parts[-1]
request = KeyRequest(k)
with self.open() as kp:
kp_entry = kp.find_entries(path=path, first=True, title=entry_name)
output[k] = self._get_field_value(kp_entry, field_name)
kp_entry = kp.find_entries(path=request.path, first=True, title=request.entry_name)
output[k] = self._get_field_value(kp_entry, request.field_name)
return output
@staticmethod

View file

@ -20,11 +20,11 @@ dependencies = [
"keyring>=25.6.0",
]
[project.optional-dependencies]
[dependency-groups]
dev = [
"httpx>=0.27.0",
"pytest>=8.3.0",
"pytest-asyncio>=0.23.0",
"pytest==9.0.1",
"pytest-asyncio>=1.3.0",
"ruff>=0.6.0",
"mypy>=1.11.0",
"types-Jinja2>=2.11.9",

View file

@ -1,3 +1,5 @@
value: %{sample}
nested: %{some.nested.value}
custom: ${custom.field}
value: ${sample}
nested: ${some.nested.value}
custom: ${custom/field}
uname: ${sample/username}
invalid: ${double/slash/example}

View file

@ -1,24 +1,50 @@
import os
from pathlib import Path
from unittest import TestCase
import pytest
import yaml
from app.services import Passwords
from app.services.mo import Mo
class TestMo(TestCase):
def test_process(self):
target_path = Path('tests/files/test1/test.yaml')
@pytest.fixture(scope='class')
def target_path():
p = Path('tests/files/test1/test.yaml')
# posprzątaj przed testem, gdyby plik istniał z poprzednich uruchomień
if p.exists():
p.unlink()
yield p
# sprzątanie po teście
if p.exists():
p.unlink()
@pytest.fixture(scope='class')
def test1_content(target_path: Path):
mo = Mo(Passwords())
mo.process(Path('tests/files/test1/test.mo.yaml').absolute())
self.assertTrue(os.path.exists(target_path))
with open(target_path, 'r') as f:
content = f.read()
self.assertFalse(content.__contains__('${'))
self.assertFalse(content.__contains__('%{'))
parsed = yaml.load(content, Loader=yaml.FullLoader)
self.assertEqual('some_pass', parsed['value'])
self.assertEqual('nested_pass', parsed['nested'])
self.assertEqual('custom_content', parsed['custom'])
assert target_path.exists()
content = target_path.read_text()
assert '${' not in content
yield yaml.load(content, Loader=yaml.FullLoader)
class TestParsing:
def test_simple(self, test1_content: dict):
assert test1_content['value'] == 'some_pass'
def test_nested(self, test1_content: dict):
assert test1_content['nested'] == 'nested_pass'
def test_custom_field(self, test1_content: dict):
assert test1_content['custom'] == 'custom_content'
def test_username_field(self, test1_content: dict):
assert test1_content['uname'] == 'sample_username'
def test_invalid_key(self, test1_content: dict):
assert test1_content.get('invalid') == 'None'