• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bergercookie / taskwarrior-syncall / 28338033595

28 Jun 2026 10:24PM UTC coverage: 57.348% (+0.4%) from 56.914%
28338033595

push

github

web-flow
Upgrade min python version, switch to `uv` and `ruff` (#164)

* Upgrade min python version to 3.11, and add support for 3.13 and 3.14
* Switch to using `uv` for installing dependencies and running commands,
  instead of `poetry`
* Switch to using `ruff` for linting and formatting, instead of `black`
  and `isort`
* Add `devbox` support for managing development environment and
  dependencies
* Upgrade dependencies to latest versions, and fix tests accordingly
  (gpsoauth, gkeepapi)
* Drop direct dependency to setuptools, we can use importlib.metadata
  instead
* [stip WIP] Add `just` support for running commands and scripts, and add a `justfile`
  for common tasks `justfile`

72 of 102 new or added lines in 31 files covered. (70.59%)

1717 of 2994 relevant lines covered (57.35%)

0.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

31.94
/syncall/scripts/fs_gkeep_sync.py
1
import sys
1✔
2
from collections.abc import Sequence
1✔
3
from pathlib import Path
1✔
4

5
import click
1✔
6
from bubop import check_optional_mutually_exclusive, format_dict, logger, loguru_tqdm_sink
1✔
7

8
from syncall.app_utils import confirm_before_proceeding, inform_about_app_extras
1✔
9

10
try:
1✔
11
    from syncall.filesystem.filesystem_side import FilesystemSide
1✔
12
    from syncall.google.gkeep_note_side import GKeepNoteSide
1✔
13
except ImportError:
×
14
    inform_about_app_extras(["gkeep", "fs"])
×
15

16

17
from syncall.aggregator import Aggregator
1✔
18
from syncall.app_utils import (
1✔
19
    app_log_to_syslog,
20
    cache_or_reuse_cached_combination,
21
    fetch_app_configuration,
22
    get_resolution_strategy,
23
    gkeep_read_username_password_token,
24
    register_teardown_handler,
25
    write_to_pass_manager,
26
)
27
from syncall.cli import (
1✔
28
    opt_filename_extension,
29
    opt_filesystem_root,
30
    opt_gkeep_ignore_labels,
31
    opt_gkeep_labels,
32
    opts_gkeep,
33
    opts_miscellaneous,
34
)
35
from syncall.filesystem_gkeep_utils import (
1✔
36
    convert_filesystem_file_to_gkeep_note,
37
    convert_gkeep_note_to_filesystem_file,
38
)
39
from syncall.google.gkeep_note_side import GKeepNoteSide
1✔
40

41

42
@click.command()
1✔
43
@opt_gkeep_labels()
1✔
44
@opt_gkeep_ignore_labels()
1✔
45
@opts_gkeep()
1✔
46
@opt_filename_extension()
1✔
47
@opt_filesystem_root()
1✔
48
@opts_miscellaneous("Filesystem", "Google Keep")
1✔
49
def main(
1✔
50
    filesystem_root: str | None,
51
    filename_extension: str,
52
    gkeep_labels: Sequence[str],
53
    gkeep_ignore_labels: Sequence[str],
54
    gkeep_user_pass_path: str,
55
    gkeep_passwd_pass_path: str,
56
    gkeep_token_pass_path: str,
57
    resolution_strategy: str,
58
    verbose: int,
59
    combination_name: str,
60
    custom_combination_savename: str,
61
    pdb_on_error: bool,
62
    confirm: bool,
63
):
64
    """Synchronize Notes from your Google Keep with text files in a directory on your filesystem.
65

66
    You can only synchronize a subset of your Google Keep notes based on a set of provided
67
    labels and you can specify where to create the files by specifying the path to a local
68
    directory. If you don't specify Google Keep Labels it will synchronize all your Google Keep
69
    notes.
70

71
    For each Google Keep Note, fs_gkeep_sync will create a corresponding file under the
72
    specified root directory with a matching name. Any addition, deletion and modification of
73
    the files on the filesystem will result in the corresponding addition, deletion and
74
    modification of the corresponding Google Keep item. The same holds the other way around.
75
    """
76
    # setup logger ----------------------------------------------------------------------------
77
    loguru_tqdm_sink(verbosity=verbose)
×
78
    app_log_to_syslog()
×
79
    logger.debug("Initialising...")
×
80
    inform_about_config = False
×
81

82
    # cli validation --------------------------------------------------------------------------
83
    check_optional_mutually_exclusive(gkeep_labels, gkeep_ignore_labels)
×
84
    check_optional_mutually_exclusive(combination_name, custom_combination_savename)
×
85
    combination_of_filesystem_root_and_gkeep_labels_and_gkeep_ignore_labels = any(
×
86
        [
87
            filesystem_root,
88
            gkeep_labels,
89
            gkeep_ignore_labels,
90
        ],
91
    )
92
    check_optional_mutually_exclusive(
×
93
        combination_name,
94
        combination_of_filesystem_root_and_gkeep_labels_and_gkeep_ignore_labels,
95
    )
96

97
    filesystem_root_path = None
×
98
    if filesystem_root is not None:
×
99
        filesystem_root_path = Path(filesystem_root)
×
100
        if not filesystem_root_path.is_dir():
×
101
            logger.error(
×
102
                "An existing directory must be provided for the synchronization ->"
103
                f" {filesystem_root_path}",
104
            )
105
            return 1
×
106

107
    # Let's strip any "."s in the name of this configuration - may mess up the PrefsManager
NEW
108
    filename_extension = filename_extension.removeprefix(".")
×
109

110
    # existing combination name is provided ---------------------------------------------------
111
    if combination_name is not None:
×
112
        app_config = fetch_app_configuration(
×
113
            side_A_name="Filesystem",
114
            side_B_name="Google Keep",
115
            combination=combination_name,
116
        )
117
        filesystem_root_path = Path(app_config["filesystem_root"])
×
118
        gkeep_labels = app_config["gkeep_labels"]
×
119
        gkeep_ignore_labels = app_config["gkeep_ignore_labels"]
×
120
        filename_extension = app_config["filename_extension"]
×
121

122
    # combination manually specified ----------------------------------------------------------
123
    else:
124
        inform_about_config = True
×
125
        combination_name = cache_or_reuse_cached_combination(
×
126
            config_args={
127
                "filesystem_root": filesystem_root,
128
                "gkeep_labels": gkeep_labels,
129
                "gkeep_ignore_labels": gkeep_ignore_labels,
130
                "filename_extension": filename_extension,
131
            },
132
            config_fname="fs_gkeep_configs",
133
            custom_combination_savename=custom_combination_savename,
134
        )
135

136
    # more checks -----------------------------------------------------------------------------
137
    if filesystem_root_path is None:
×
138
        logger.error(
×
139
            "You have to provide at least one valid filesystem root path to use for "
140
            " synchronization. You can do so either via CLI arguments or by specifying an"
141
            " existing saved combination",
142
        )
143
        sys.exit(1)
×
144

145
    if not gkeep_labels and not gkeep_ignore_labels:
×
146
        logger.error(
×
147
            "Refusing to run without any Google Keep labels to keep or remove - please provide"
148
            " at least one of these two to continue",
149
        )
150
        sys.exit(1)
×
151

152
    # announce configuration ------------------------------------------------------------------
153
    logger.info(
×
154
        format_dict(
155
            header="Configuration",
156
            items={
157
                "Filesystem Root": filesystem_root,
158
                "Google Keep Labels": gkeep_labels,
159
                "Google Keep Labels to Ignore": gkeep_ignore_labels,
160
                "Filename Extension": filename_extension,
161
            },
162
            prefix="\n\n",
163
            suffix="\n",
164
        ),
165
    )
166
    if confirm:
×
167
        confirm_before_proceeding()
×
168

169
    # initialize sides ------------------------------------------------------------------------
170
    gkeep_user, gkeep_passwd, gkeep_token = gkeep_read_username_password_token(
×
171
        gkeep_user_pass_path,
172
        gkeep_passwd_pass_path,
173
        gkeep_token_pass_path,
174
    )
175

176
    gkeep_side = GKeepNoteSide(
×
177
        gkeep_labels=gkeep_labels,
178
        gkeep_ignore_labels=gkeep_ignore_labels,
179
        gkeep_user=gkeep_user,
180
        gkeep_passwd=gkeep_passwd,
181
        gkeep_token=gkeep_token,
182
    )
183

184
    filesystem_side = FilesystemSide(
×
185
        filesystem_root=filesystem_root_path,
186
        filename_extension=filename_extension,
187
    )
188

189
    # teardown function and exception handling ------------------------------------------------
190
    register_teardown_handler(
×
191
        pdb_on_error=pdb_on_error,
192
        inform_about_config=inform_about_config,
193
        combination_name=combination_name,
194
        verbose=verbose,
195
    )
196

197
    # take extra arguments into account -------------------------------------------------------
198
    def converter_A_to_B(gkeep_note):
×
199
        return convert_gkeep_note_to_filesystem_file(
×
200
            gkeep_note=gkeep_note,
201
            filename_extension=filename_extension,
202
            filesystem_root=filesystem_root_path,
203
        )
204

205
    converter_A_to_B.__doc__ = convert_gkeep_note_to_filesystem_file.__doc__
×
206

207
    # sync ------------------------------------------------------------------------------------
208
    with Aggregator(
×
209
        side_A=gkeep_side,
210
        side_B=filesystem_side,
211
        converter_B_to_A=convert_filesystem_file_to_gkeep_note,
212
        converter_A_to_B=converter_A_to_B,
213
        resolution_strategy=get_resolution_strategy(
214
            resolution_strategy,
215
            side_A_type=type(gkeep_side),
216
            side_B_type=type(filesystem_side),
217
        ),
218
        config_fname=combination_name,
219
        ignore_keys=(
220
            (),
221
            (),
222
        ),
223
    ) as aggregator:
224
        aggregator.sync()
×
225

226
    # cache the token -------------------------------------------------------------------------
227
    token = gkeep_side.get_master_token()
×
228
    if token is not None:
×
229
        logger.debug(f"Caching the gkeep token in pass -> {gkeep_token_pass_path}...")
×
230
        write_to_pass_manager(password_path=gkeep_token_pass_path, passwd=token)
×
231

232
    return 0
×
233

234

235
if __name__ == "__main__":
1✔
236
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc