Skip to content

Commit

Permalink
Merge pull request #15 from aptly-io/feature_re_selection
Browse files Browse the repository at this point in the history
feat: select items that match a regular expression

Matching is based on the "{folder}/{title}" value. Notice the "/" inserted between folder and title value as was it a directory and a filename.
  • Loading branch information
aptly-io authored Mar 5, 2023
2 parents 09ec715 + 8c511bf commit 3f9880e
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8"]
python-version: ["3.9"]
fail-fast: false
steps:
- uses: actions/checkout@v2
Expand All @@ -21,7 +21,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt update
sudo apt-get install -y --upgrade git build-essential python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-dev curl
sudo apt-get install -y --upgrade git build-essential python3-venv python3-dev curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ pwsync --from demo/from.kdbx --to bitwarden
# see below for an example of typical output.
pwsync --from demo/from.kdbx --to demo/to.kdbx

# Do a dry-run (-d) for syncing all (-a) items that match the selection (-s)
# Item's are matched based on the concatenation of their <folder-value> '/' <title-value>
python -mpwsync.main -d -f demo/from.kdbx -t demo/to.kdbx -a \
-s ".*/folder 1.1/.*" -s "folder 2/.*" \
--from-master-password=pw --to-master-password=pw

# a description of all options
pwsync -h
```
Expand Down Expand Up @@ -198,4 +204,4 @@ An curated dump of the console output is shown below:

`pwsync` is necessarily GPL3 since it (currently) depends upon the GPL3 python module `pykeepass`.

Copyright 2022 Francis Meyvis ([email protected])
Copyright 2022, 2023 Francis Meyvis ([email protected])
4 changes: 2 additions & 2 deletions pwsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

import sys

assert sys.version_info >= (3, 7)

from .bw_cli_wrapper import BitwardenClientWrapper
from .common import PwsDuplicate, PwsMissingOrganization, PwsUnsupported
from .dataset import PasswordDataset
from .item import PwsItem
from .kp_db_cli import KeepassDatabaseClient
from .sync import PwsSyncer

assert sys.version_info >= (3, 7)
2 changes: 1 addition & 1 deletion pwsync/bw_cli_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def _check_output(
input_value=None,
):
try:
result_json = check_output(cmd, input=input_value, env=self._env)
result_json = check_output(cmd, input=input_value, env=self._env).decode("utf-8")
self._logger.debug("cmd: %s, result: %s", cmd, result_json)
return json.loads(result_json)
except CalledProcessError as exc:
Expand Down
5 changes: 3 additions & 2 deletions pwsync/common.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Copyright 2022 Francis Meyvis ([email protected])
# Copyright 2022, 2023 Francis Meyvis ([email protected])

"""common constants and helper functions"""

from dataclasses import dataclass
from typing import List
from typing import List, Optional

LOGGER_NAME = "PWSYNC"

Expand Down Expand Up @@ -57,6 +57,7 @@ class PwsQueryInfo:
ids: List[str]
id_sep: str = ":"
sync: bool = True
filters: Optional[List[str]] = None


@dataclass
Expand Down
45 changes: 37 additions & 8 deletions pwsync/console.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Copyright 2022 Francis Meyvis ([email protected])
# Copyright 2022, 2023 Francis Meyvis ([email protected])

"""interactive synchronize using the console"""

import sys
from difflib import SequenceMatcher
from os.path import join
from typing import List, Optional

from prompt_toolkit import HTML
Expand Down Expand Up @@ -184,9 +185,23 @@ def _sync_element(kind: str, element: PwsDiffElement, to_db: PwsDatabaseClient):
raise PwsUnsupported(f"_sync_element({kind})")


def _sync_section(
kind: str, query_info: PwsQueryInfo, syncer: PwsSyncer, run_options: RunOptions, to_dataset: PasswordDataset
):
def _match_selector(selector, item: Optional[PwsItem]) -> bool:
if item is None:
return False
path = join(item.folder if item.folder is not None else "", item.title)
return selector.fullmatch(path) is not None


def _match_selectors(selectors, element):
for selector in selectors:
if _match_selector(selector, element.from_item):
return True
if _match_selector(selector, element.to_item):
return True
return False # non-matching element skipped


def _switch_kind(kind: str, query_info: PwsQueryInfo, syncer):
def _get_key_using_from_item(diff_element: PwsDiffElement):
return diff_element.from_item.make_id(query_info) if diff_element.from_item else ""

Expand All @@ -211,14 +226,28 @@ def _get_key_using_to_item(diff_element: PwsDiffElement):
key_getter = _get_key_using_from_item
props_name = "add_props"
elif kind == "skipped":
return # TODO handle skipped
raise NotImplementedError("kind 'skipped' not handled")
elif kind == "unchanged":
return # TODO handle skipped
raise NotImplementedError("kind 'unchanged' not handled")

if query_info.filters:
filtered_data = list(filter(lambda e: _match_selectors(query_info.filters, e), data))
else:
filtered_data = data

return filtered_data, key_getter, props_name


def _sync_section(
kind: str, query_info: PwsQueryInfo, syncer: PwsSyncer, run_options: RunOptions, to_dataset: PasswordDataset
):
data, key_getter, props_name = _switch_kind(kind, query_info, syncer)

print_ft(HTML(_markup(f"To {kind}: {len(data)}", "info")), style=STYLE)

section_folder = ""
count = 0

for element in sorted(data, key=key_getter):
count += 1
section_folder = _print_ft_element(
Expand Down Expand Up @@ -249,5 +278,5 @@ def console_sync(query_info: PwsQueryInfo, syncer: PwsSyncer, run_options: RunOp
_sync_section("update", query_info, syncer, run_options, to_dataset)
_sync_section("create", query_info, syncer, run_options, to_dataset)
_sync_section("delete", query_info, syncer, run_options, to_dataset)
_sync_section("skipped", query_info, syncer, run_options, to_dataset)
_sync_section("unchanged", query_info, syncer, run_options, to_dataset)
# _sync_section("skipped", query_info, syncer, run_options, to_dataset) # TODO handle skipped
# _sync_section("unchanged", query_info, syncer, run_options, to_dataset) # TODO handle unchanged
15 changes: 13 additions & 2 deletions pwsync/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env python3

# Copyright 2022 Francis Meyvis ([email protected])
# Copyright 2022, 2023 Francis Meyvis ([email protected])

""" password sync's entry point"""

import re
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from dataclasses import dataclass
from getpass import getpass
Expand Down Expand Up @@ -125,6 +126,15 @@ def _parse_command_line():
+ "or stored in the shell history buffer, etc. If left empty, it is prompted for on the command line",
)

parser.add_argument(
"-s",
"--select",
action="append",
nargs="*",
default=[],
help="Select the items to sync by matching their folder/title value with the regular expression.",
)

parser.add_argument("-U", "--auto-update", action="store_true", help="automatically update all entries")
parser.add_argument("-C", "--auto-create", action="store_true", help="automatically create all entries")

Expand Down Expand Up @@ -211,7 +221,8 @@ def main():
logger.addHandler(file_handler)
logger.propagate = False # do not further propagate to the root handler (stdout)

query_info = PwsQueryInfo(args.id.split(","), args.id_sep, args.sync)
filters = list(map(re.compile, map(lambda i: i[0], args.select)))
query_info = PwsQueryInfo(args.id.split(","), args.id_sep, args.sync, filters)

access = _AccessInfo(args.from_username, args.from_secret, args.from_master_password)
from_dataset = _open_password_db(query_info, "from", getattr(args, "from"), access)
Expand Down
2 changes: 1 addition & 1 deletion pwsync/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def sync(self):
elif diff_element.action == DiffSyncActions.UPDATE:
self._update(diff_element)
else:
raise Exception(f"Unexpected action: {diff_element.action}")
raise NotImplementedError(f"Unexpected action: {diff_element.action}")

self._logger.info("diff.summary: %s", diff.summary())
self._logger.debug("self.creates: %s", self.creates)
Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
url="https://github.com/aptly-io/pwsync",
classifiers=[
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: POSIX :: Linux", # TODO test on other OSes
Expand All @@ -34,8 +33,8 @@
packages=find_packages(),
install_requires=[
"pykeepass==4.0.3",
"diffsync==1.6.0",
"prompt-toolkit==3.0.30",
"diffsync==1.7.0",
"prompt-toolkit==3.0.38",
],
extras_require={
"dev": [
Expand Down

0 comments on commit 3f9880e

Please sign in to comment.