mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-03-25 08:55:50 +00:00
cli/discover: add implicit config to pair for collection creation
Adds support for auto-creating collections when they exist only on one side and `implicit = 'create'` is set in the pair config.
This commit is contained in:
parent
ac9919d865
commit
cbb4e314f6
6 changed files with 96 additions and 4 deletions
|
|
@ -73,6 +73,10 @@ Version 0.19.0
|
||||||
- Add a new ``showconfig`` status. This prints *some* configuration values as
|
- Add a new ``showconfig`` status. This prints *some* configuration values as
|
||||||
JSON. This is intended to be used by external tools and helpers that interact
|
JSON. This is intended to be used by external tools and helpers that interact
|
||||||
with ``vdirsyncer``, and considered experimental.
|
with ``vdirsyncer``, and considered experimental.
|
||||||
|
- Add ``implicit`` option to the :ref:`pair section <pair_config>`. When set to
|
||||||
|
"create", it implicitly creates missing collections during sync without user
|
||||||
|
prompts. This simplifies workflows where collections should be automatically
|
||||||
|
created on both sides.
|
||||||
- Update TLS-related tests that were failing due to weak MDs. :gh:`903`
|
- Update TLS-related tests that were failing due to weak MDs. :gh:`903`
|
||||||
- ``pytest-httpserver`` and ``trustme`` are now required for tests.
|
- ``pytest-httpserver`` and ``trustme`` are now required for tests.
|
||||||
- ``pytest-localserver`` is no longer required for tests.
|
- ``pytest-localserver`` is no longer required for tests.
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,16 @@ Pair Section
|
||||||
|
|
||||||
The ``conflict_resolution`` parameter applies for these properties too.
|
The ``conflict_resolution`` parameter applies for these properties too.
|
||||||
|
|
||||||
|
.. _implicit_def:
|
||||||
|
|
||||||
|
- ``implicit``: Opt into implicitly creating collections. Example::
|
||||||
|
|
||||||
|
implicit = "create"
|
||||||
|
|
||||||
|
When set to "create", missing collections are automatically created on both
|
||||||
|
sides during sync without prompting the user. This simplifies workflows where
|
||||||
|
all collections should be synchronized bidirectionally.
|
||||||
|
|
||||||
.. _storage_config:
|
.. _storage_config:
|
||||||
|
|
||||||
Storage Section
|
Storage Section
|
||||||
|
|
|
||||||
|
|
@ -222,3 +222,62 @@ def test_validate_collections_param():
|
||||||
x([["c", None, "b"]])
|
x([["c", None, "b"]])
|
||||||
x([["c", "a", None]])
|
x([["c", "a", None]])
|
||||||
x([["c", None, None]])
|
x([["c", None, None]])
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_implicit_value(read_config):
|
||||||
|
expected_message = "`implicit` parameter must be 'create' or absent"
|
||||||
|
with pytest.raises(exceptions.UserError) as excinfo:
|
||||||
|
read_config(
|
||||||
|
"""
|
||||||
|
[general]
|
||||||
|
status_path = "/tmp/status/"
|
||||||
|
|
||||||
|
[pair my_pair]
|
||||||
|
a = "my_a"
|
||||||
|
b = "my_b"
|
||||||
|
collections = null
|
||||||
|
implicit = "invalid"
|
||||||
|
|
||||||
|
[storage my_a]
|
||||||
|
type = "filesystem"
|
||||||
|
path = "{base}/path_a/"
|
||||||
|
fileext = ".txt"
|
||||||
|
|
||||||
|
[storage my_b]
|
||||||
|
type = "filesystem"
|
||||||
|
path = "{base}/path_b/"
|
||||||
|
fileext = ".txt"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected_message in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_implicit_create_only(read_config):
|
||||||
|
"""Test that implicit create works."""
|
||||||
|
errors, c = read_config(
|
||||||
|
"""
|
||||||
|
[general]
|
||||||
|
status_path = "/tmp/status/"
|
||||||
|
|
||||||
|
[pair my_pair]
|
||||||
|
a = "my_a"
|
||||||
|
b = "my_b"
|
||||||
|
collections = ["from a", "from b"]
|
||||||
|
implicit = "create"
|
||||||
|
|
||||||
|
[storage my_a]
|
||||||
|
type = "filesystem"
|
||||||
|
path = "{base}/path_a/"
|
||||||
|
fileext = ".txt"
|
||||||
|
|
||||||
|
[storage my_b]
|
||||||
|
type = "filesystem"
|
||||||
|
path = "{base}/path_b/"
|
||||||
|
fileext = ".txt"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not errors
|
||||||
|
pair = c.pairs["my_pair"]
|
||||||
|
assert pair.implicit == "create"
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,14 @@ def _validate_collections_param(collections):
|
||||||
raise ValueError(f"`collections` parameter, position {i}: {e!s}")
|
raise ValueError(f"`collections` parameter, position {i}: {e!s}")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_implicit_param(implicit):
|
||||||
|
if implicit is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if implicit != "create":
|
||||||
|
raise ValueError("`implicit` parameter must be 'create' or absent.")
|
||||||
|
|
||||||
|
|
||||||
class _ConfigReader:
|
class _ConfigReader:
|
||||||
def __init__(self, f: IO[Any]):
|
def __init__(self, f: IO[Any]):
|
||||||
self._file: IO[Any] = f
|
self._file: IO[Any] = f
|
||||||
|
|
@ -230,6 +238,7 @@ class PairConfig:
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.name_a: str = options.pop("a")
|
self.name_a: str = options.pop("a")
|
||||||
self.name_b: str = options.pop("b")
|
self.name_b: str = options.pop("b")
|
||||||
|
self.implicit = options.pop("implicit", None)
|
||||||
|
|
||||||
self._partial_sync: str | None = options.pop("partial_sync", None)
|
self._partial_sync: str | None = options.pop("partial_sync", None)
|
||||||
self.metadata: str | tuple[()] = options.pop("metadata", ())
|
self.metadata: str | tuple[()] = options.pop("metadata", ())
|
||||||
|
|
@ -248,6 +257,7 @@ class PairConfig:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_validate_collections_param(self.collections)
|
_validate_collections_param(self.collections)
|
||||||
|
_validate_implicit_param(self.implicit)
|
||||||
|
|
||||||
if options:
|
if options:
|
||||||
raise ValueError("Unknown options: {}".format(", ".join(options)))
|
raise ValueError("Unknown options: {}".format(", ".join(options)))
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,13 @@ async def collections_for_pair(
|
||||||
connector=connector,
|
connector=connector,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _handle_collection_not_found(
|
||||||
|
config, collection, e=None, implicit_create=False
|
||||||
|
):
|
||||||
|
return await handle_collection_not_found(
|
||||||
|
config, collection, e=e, implicit_create=pair.implicit == "create"
|
||||||
|
)
|
||||||
|
|
||||||
# We have to use a list here because the special None/null value would get
|
# We have to use a list here because the special None/null value would get
|
||||||
# mangled to string (because JSON objects always have string keys).
|
# mangled to string (because JSON objects always have string keys).
|
||||||
rv = await aiostream.stream.list( # type: ignore[assignment]
|
rv = await aiostream.stream.list( # type: ignore[assignment]
|
||||||
|
|
@ -102,7 +109,7 @@ async def collections_for_pair(
|
||||||
config_b=pair.config_b,
|
config_b=pair.config_b,
|
||||||
get_a_discovered=a_discovered.get_self,
|
get_a_discovered=a_discovered.get_self,
|
||||||
get_b_discovered=b_discovered.get_self,
|
get_b_discovered=b_discovered.get_self,
|
||||||
_handle_collection_not_found=handle_collection_not_found,
|
_handle_collection_not_found=_handle_collection_not_found,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -286,7 +286,7 @@ async def storage_instance_from_config(
|
||||||
except exceptions.CollectionNotFound as e:
|
except exceptions.CollectionNotFound as e:
|
||||||
if create:
|
if create:
|
||||||
config = await handle_collection_not_found(
|
config = await handle_collection_not_found(
|
||||||
config, config.get("collection", None), e=str(e)
|
config, config.get("collection", None), e=str(e), implicit_create=True
|
||||||
)
|
)
|
||||||
return await storage_instance_from_config(
|
return await storage_instance_from_config(
|
||||||
config,
|
config,
|
||||||
|
|
@ -341,7 +341,9 @@ def assert_permissions(path: str, wanted: int) -> None:
|
||||||
os.chmod(path, wanted)
|
os.chmod(path, wanted)
|
||||||
|
|
||||||
|
|
||||||
async def handle_collection_not_found(config, collection, e=None):
|
async def handle_collection_not_found(
|
||||||
|
config, collection, e=None, implicit_create=False
|
||||||
|
):
|
||||||
storage_name = config.get("instance_name", None)
|
storage_name = config.get("instance_name", None)
|
||||||
|
|
||||||
cli_logger.warning(
|
cli_logger.warning(
|
||||||
|
|
@ -350,7 +352,7 @@ async def handle_collection_not_found(config, collection, e=None):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if click.confirm("Should vdirsyncer attempt to create it?"):
|
if implicit_create or click.confirm("Should vdirsyncer attempt to create it?"):
|
||||||
storage_type = config["type"]
|
storage_type = config["type"]
|
||||||
cls, config = storage_class_from_config(config)
|
cls, config = storage_class_from_config(config)
|
||||||
config["collection"] = collection
|
config["collection"] = collection
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue