Compare commits

..

514 commits

Author SHA1 Message Date
Hugo Osvaldo Barrera
c3262d88cc Mark unused variables as such 2025-11-06 01:05:48 +01:00
cbb4e314f6 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.
2025-11-06 00:04:22 +00:00
Hugo Osvaldo Barrera
ac9919d865 Add changelog entry for latest commits 2025-10-09 11:05:33 +02:00
samm81
b124ce835b fix: remove unused import 2025-10-09 09:02:01 +00:00
samm81
6708dbbbdc fix: fix ssl behavior in request
- `ClientConnectionError` in `aiohttp` can wrap SSL handshake and
  certificate verification errors
- Retrying those hides the real cause and produced
  `TransientNetworkError` instead of the expected certificate error
- Removing `ClientConnectionError` from the transient list lets SSL
  errors surface correctly
2025-10-09 09:02:01 +00:00
samm81
81d8444810 http: refactor auth loop 2025-10-09 09:02:01 +00:00
samm81
4990cdf229 http: retry safe DAV methods on transient aiohttp disconnects; cli: gather with return_exceptions to allow in-flight backoffs to finish
- Retry ServerDisconnectedError/ServerTimeoutError/ClientConnectionError/asyncio.TimeoutError for GET/HEAD/OPTIONS/PROPFIND/REPORT
- Keep original rate-limit handling (429, Google 403 usageLimits)
- In CLI, avoid cancelling sibling tasks so per-request backoff can complete; re-raise first failure after all tasks finish
2025-10-09 09:02:01 +00:00
Hugo Osvaldo Barrera
4c2c60402e ci: run ruff and mypy
Fixes: https://github.com/pimutils/vdirsyncer/issues/1194
2025-09-20 13:53:39 +02:00
Hugo Osvaldo Barrera
2f4f4ac72b Fix some mypy type failures 2025-09-20 13:51:21 +02:00
Hugo Osvaldo Barrera
6354db82c4 make: install check requirements via install-dev 2025-09-20 13:19:41 +02:00
Hugo Osvaldo Barrera
a9b6488dac Merge docs-requirements.txt into pyproject.toml
Keep requirements definitions all in one place.
2025-09-20 13:18:17 +02:00
Hugo Osvaldo Barrera
a4ceabf80b Organise imports
And update imports from deprecated locations.
2025-09-20 13:05:14 +02:00
Hugo Osvaldo Barrera
3488f77cd6 Remove unused variables 2025-09-20 13:05:14 +02:00
Hugo Osvaldo Barrera
19120422a7 Use ternary operator for trivial assignment 2025-09-20 13:05:14 +02:00
Hugo Osvaldo Barrera
2e619806a0 Drop support for Python 3.8
Note that recent commits introduced syntax unsupported by Python 3.8
already.
2025-09-20 13:05:03 +02:00
Hugo Osvaldo Barrera
4669bede07 Organise imports 2025-09-20 12:56:22 +02:00
Hugo Osvaldo Barrera
59c1c55407 Document wrapper 2025-09-20 12:50:00 +02:00
Hugo Osvaldo Barrera
1502f5b5f4 Execute one assertion per line 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
a4d4bf8fd1 Normalise pytest syntax 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
aab70e9fb0 Use cached_property from the stdlib
Our local implementation preceded the one in the stdlib, but we no
longer support versions of Python which do not ship it.
2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
ed88406aec Avoid using mutable class attributes
A tuple works fine here.
2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
ffe883a2f1 Avoid warning due to unused import 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
e5f2869580 ruff: ignore block for legacy Python 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
95bb7bd7f9 Declare functions instead of assigning to lambdas 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
e3b2473383 Use list expansion instead of concatenation 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
424cfc5799 ruff: ignore false positive 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
29312e87c5 Close status even if assertions fail 2025-09-20 12:45:56 +02:00
Hugo Osvaldo Barrera
c77b22334a Add changelog entry for latest change 2025-09-20 12:42:10 +02:00
samm81
02350c924b
http(request): collates status checks 2025-09-13 16:23:46 +07:00
Samuel Maynard
605f878f9b
test_retry: remove unneeded decorator
Co-authored-by: Hugo <hugo@whynothugo.nl>
2025-09-13 12:22:25 +03:00
samm81
bb2b71da81
builds(archlinux-py313): adds python-tenacity package 2025-09-12 17:02:28 +07:00
samm81
065ebe4752
AUTHORS: add samm81 2025-09-12 16:25:00 +07:00
samm81
0d741022a9
http: add rate limiting (mainly for google)
- google calendar uses the `403` and `429` codes to perform rate limiting [1][2]. this pr adds `tenacity` to perform exponential back off as suggested in google calendar's docs [3].

[1]: https://developers.google.com/workspace/calendar/api/guides/errors#403_rate_limit_exceeded
[2]: https://developers.google.com/workspace/calendar/api/guides/errors#429_too_many_requests
[3]: https://developers.google.com/workspace/calendar/api/guides/quota#backoff
2025-09-12 16:20:44 +07:00
Hugo Osvaldo Barrera
b5d3b7e578 Apply auto-fixes for RUF rule 2025-08-29 10:17:44 +02:00
Hugo Osvaldo Barrera
9677cf9812 Simplify some statements 2025-08-29 10:17:44 +02:00
Hugo Osvaldo Barrera
6da84c7881 ruff: sort rules 2025-08-29 10:17:44 +02:00
Hugo Osvaldo Barrera
dceb113334 ruff: fix mix-up in configuration
Ruff hasn't been finding errors in a while. Most of them are linting
checks anyway, but there was quite a bit of deprecated usages.
2025-08-29 10:17:44 +02:00
Hugo Osvaldo Barrera
01fa614b6b Fix line which are too long 2025-08-29 10:17:41 +02:00
Hugo Osvaldo Barrera
20cc1247ed ruff: apply auto-fixes 2025-08-29 10:03:24 +02:00
Дилян Палаузов
2f548e048d Some code simplifications with the return statement 2025-08-29 09:48:27 +02:00
Jakub Klinkovský
5d343264f3 Remove python-requests-toolbelt from Arch Linux build
The dependency was dropped in 89a01631fa
2025-08-29 09:28:50 +02:00
Hugo Osvaldo Barrera
bc3fa8bd39 Remove stale references to setup.py 2025-08-28 22:57:55 +02:00
Hugo Osvaldo Barrera
8803d5a086 ruff: use extend-select
Ensure that we don't disable any default rules.
2025-08-28 11:40:46 +02:00
Hugo Osvaldo Barrera
96754a3d0a ruff: enable TID rules 2025-08-28 11:39:06 +02:00
Hugo Osvaldo Barrera
d42707c108 Bump constraint for aiostream
There's a newer version available, and it also doesn't have any breaking
changes which could affect us.
2025-08-28 11:37:14 +02:00
Hugo Osvaldo Barrera
ddfe3cc749 Bump constraint for aiostream
Fixes: https://github.com/pimutils/vdirsyncer/issues/1111
2025-08-28 11:32:28 +02:00
Radon Rosborough
84ff0ac943 Log error response body in debug 2025-08-27 09:11:32 +02:00
Hugo Osvaldo Barrera
388c16f188 Document sqlite fix in changelog 2025-08-25 17:37:26 +02:00
Hugo Osvaldo Barrera
78f41d32ce Explicitly close status database
Using `__del__` often closes the database on a different thread, which
is not supported by the sqlite module and produces a different warning.

Explicitly close the status database everywhere it is used.
2025-08-25 17:33:20 +02:00
Hugo Osvaldo Barrera
164559ad7a Remove references to obsolete event_loop fixture
It's gone from the latest pytest-asyncio.
2025-08-25 17:12:21 +02:00
samm81
2c6dc4cddf updates SqliteStatus to properly close connections
otherwise, when trying to run `pytest` in a `python3.13` environment
results in a bunch of

```
tests/unit/sync/test_sync.py::test_partial_sync_ignore
  /home/user/.asdf/installs/python/3.13.1/lib/python3.13/asyncio/base_events.py:650: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x7fda8f6b6c50>
    sys.set_asyncgen_hooks(
  Enable tracemalloc to get traceback where the object was allocated.
  See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
  ```
2025-08-25 16:53:00 +02:00
samm81
9bbb7fa91a fix: fix mypy typing error 2025-08-25 16:51:29 +02:00
Hugo Osvaldo Barrera
f8bcafa9d7 ci: use Alpine 3.19 for Python 3.11 2025-08-25 16:49:16 +02:00
Hugo Osvaldo Barrera
162879df21 ci: include python version in job name 2025-07-23 23:24:35 +02:00
Hugo Osvaldo Barrera
3b9db0e4db Add support for Python 3.13
Fixes: https://github.com/pimutils/vdirsyncer/issues/1180
2025-07-23 23:23:59 +02:00
Hugo Osvaldo Barrera
63d2e6c795 pyproject: squelch warning 2025-04-11 01:59:29 +02:00
Hugo Osvaldo Barrera
03d1c4666d pyproject: update syntax for licence 2025-04-11 01:59:17 +02:00
Hugo Osvaldo Barrera
ecdd565be4 Document checkfile() 2025-04-09 14:00:40 +02:00
Hugo Osvaldo Barrera
17e43fd633 Move test dependencies into pyproject.toml 2025-04-07 18:47:44 +02:00
Hugo Osvaldo Barrera
2b4496fea4 Update linting tools 2025-04-07 18:42:07 +02:00
Hugo Osvaldo Barrera
fc4a02c0c9 Add some missing type hints 2025-04-07 18:40:34 +02:00
Hugo Osvaldo Barrera
c19802e4d8 Configure ruff as an auto-formatter 2025-04-07 18:40:34 +02:00
Hugo Osvaldo Barrera
cce8fef8de Auto-format using ruff 2025-04-07 18:40:34 +02:00
Hugo Osvaldo Barrera
9a0dbc8cd0 Update ruff configuration syntax 2025-04-07 18:40:34 +02:00
Hugo Osvaldo Barrera
32453cccfc Drop support for Python 3.7
Installing on Python 3.7 no longer works due to lack of support in the
minimal version of setuptools_scm. This commit makes the change
official, but it happened a while ago.
2025-04-07 18:39:52 +02:00
Hugo Osvaldo Barrera
057f3af293 Remove stale GitLab CI config 2025-04-07 18:35:12 +02:00
Hugo Osvaldo Barrera
e76d8a5b03 Add two more trove classifiers 2025-04-07 18:09:36 +02:00
Hugo Osvaldo Barrera
d8961232c4 Remove setup.py in favour of pyproject.toml
Implements: https://github.com/pimutils/vdirsyncer/issues/1164
2025-04-07 18:06:45 +02:00
Hugo Osvaldo Barrera
646e0b48a5 Delete stale comment 2025-04-07 18:01:16 +02:00
Hugo Osvaldo Barrera
fb6a859b88 Add changelog entry for recent change 2025-04-07 17:39:18 +02:00
Petr Moucha
ff999b5b74 Use proxy configuration from environment for Google storage 2025-04-04 13:17:32 +02:00
Hugo Osvaldo Barrera
41b48857eb Remove reference to dead domain 2025-03-06 11:57:05 +01:00
Hugo Osvaldo Barrera
70d09e6d5d Remove stale comment 2025-02-13 13:42:06 +01:00
Ben Boeckel
8b063c39cb atomicwrites: remove dependency on abandoned library 2025-02-13 13:37:06 +01:00
Hugo Osvaldo Barrera
12a06917db Add explicit configuration for readthedocs
See: https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/
2025-02-13 13:34:13 +01:00
Hugo Osvaldo Barrera
2fee1d67f2 Update CI job with "oldest supported dependencies"
Alpine 3.17 has faded away, bump to Alpine 3.18.
2025-02-13 13:32:59 +01:00
Hugo Osvaldo Barrera
a934d5ec66 Keep test for duplicate consecutive keys
See: https://github.com/pimutils/vdirsyncer/pull/1153
2024-12-21 16:49:50 +01:00
Colin Watson
c79d3680cd Fix _Component.__delitem__ with adjacent identical keys
Hypothesis found the following example:

```
tests/unit/utils/test_vobject.py:335: in add_prop
    assert c[key] == value
E   AssertionError: assert '0' == '1'
E
E     - 1
E     + 0
E   Falsifying example:
E   state = VobjectMachine()
E   unparsed_0 = state.get_unparsed_lines(encoded=False, joined=False)
E   parsed_0 = state.parse(unparsed=unparsed_0)
E   state.add_prop_raw(c=parsed_0, key='0', params=[], value='0')
E   state.add_prop_raw(c=parsed_0, key='0', params=[], value='0')
E   state.add_prop(c=parsed_0, key='0', value='1')
E   state.teardown()
```

After the two `add_prop_raw` calls, `c.props` is `["0;:0", "0;:0",
"FOO:YES"]`.  `_Component.__delitem__` then fails to effectively delete
the previous key: it deletes the first `"0;:0"` item, but then checks
for continuation lines following it and incorrectly keeps the second
`"0;:0"` item even though it begins with one of the prefixes it's trying
to delete.  Checking for the prefix in the check for continuation lines
fixes this.

Fixes: #1149
2024-12-20 01:43:15 +00:00
Hugo Osvaldo Barrera
cd050d57b9 Use direnv to set up a virtualenv for development 2024-12-09 14:18:24 +01:00
Hugo Osvaldo Barrera
8c98992f74 Move setuptools-scm config into pyproject.toml 2024-12-09 14:18:06 +01:00
Hugo Osvaldo Barrera
c2eed9fb59 Add a readthedocs configuration file
Used for building docs in CI pipelines.
2024-12-09 01:36:22 +01:00
Mike A.
a490544405 Do not load netrc config files 2024-12-09 01:32:29 +01:00
Hugo Osvaldo Barrera
688d6f907f Update deprecated usages of hypothesis 2024-12-09 01:30:44 +01:00
euxane
2e7e31fdbf storage/http: add support for filter_hook
This allows users to process fetched items through a filter command,
to fix malformed webcal items as they are imported.

In my case, my provider adds the export time to the description and
random sequence numbers to all events. This caused the whole collection
to be invalidated and propagated at each sync. I use the filter to
remove those, canonicalising the items.
2024-12-08 19:31:32 +01:00
Arran Ubels
616d7aacb0 OfflineIMAP url Update 2024-10-31 22:43:45 +01:00
Hugo Osvaldo Barrera
89129e37b6 Typo
Fixes: https://github.com/pimutils/vdirsyncer/issues/1139
2024-09-13 18:36:17 +02:00
Hugo Osvaldo Barrera
88722ef4b7 Add changelog entry for Digest Auth 2024-09-11 17:25:29 +02:00
Mike A.
35f299679f Rewrite guess auth test for unsupported status 2024-09-11 12:04:05 +02:00
Mike A.
67e1c0ded5 Make tests pass 2024-09-11 12:04:05 +02:00
Mike A.
89a01631fa Remove requests_toolbelt 2024-09-11 12:04:05 +02:00
Mike A.
611b8667a3 Implement digest auth 2024-09-11 12:04:05 +02:00
Hugo Osvaldo Barrera
8550475548 Formatting 2024-08-26 12:49:36 +02:00
Hugo Osvaldo Barrera
cd2445b991 Upgrade Alpine release used in CI 2024-08-26 12:49:24 +02:00
Jakub Klinkovský
5ca2742271 Add short option for the help option 2024-08-26 12:43:20 +02:00
Jakub Klinkovský
5ac9dcec29 Update documentation regarding SSL pinning by fingerprint 2024-08-16 15:18:18 +02:00
octvs
a513a7e4fa docs: update config info on todoman tutorial 2024-04-02 15:30:16 +02:00
Dick Marinus
5ae05245e6 fix pylint W0621: Redefining name 'main' from outer scope (line 68) (redefined-outer-name) 2024-03-19 09:59:08 +01:00
Hugo Osvaldo Barrera
055ed120dd Pre-commit autoupdate 2024-02-20 15:08:51 +01:00
Hugo Osvaldo Barrera
31816dc652 Add some type hints 2024-02-20 15:08:51 +01:00
Bleala
2e023a5feb Update AUTHORS.rst 2024-02-16 14:29:11 +01:00
Bleala
14afe16a13 Update CHANGELOG.rst 2024-02-16 14:29:11 +01:00
Bleala
5766e1c501 Add Docker Environment 2024-02-16 14:29:11 +01:00
Xavier Brochard
fade399a21 more explanations of "collection" meaning 2024-02-02 10:26:49 +01:00
Xavier Brochard
3433f8a034 A bit more explanation of "from a" and "from b" 2024-02-02 10:25:10 +01:00
chrisblech
6a3077f9dc add pre_deletion_hook
closes https://github.com/pimutils/vdirsyncer/issues/1107
2024-01-31 19:14:59 +01:00
Hugo Osvaldo Barrera
42c5dba208 Pre-commit autoupdate 2024-01-31 19:08:25 +01:00
Hugo Osvaldo Barrera
7991419ab1 Merge implicitly concatenated strings 2024-01-31 19:08:25 +01:00
Hugo Osvaldo Barrera
03e6afe9dc Remove broken contact link
Fixes: https://github.com/pimutils/vdirsyncer/issues/1104
2024-01-28 20:15:08 +01:00
Hugo
762d369560
Merge pull request #1103 from jasonccox/main
Require matching BEGIN and END lines in vobjects
2024-01-28 20:13:28 +01:00
Hugo Osvaldo Barrera
2396c46b04 Allow specifying deb distro/ver via env vars 2023-12-18 14:35:48 +01:00
Hugo Osvaldo Barrera
b626236128 Use docker (instead of podman) to build debs 2023-12-18 14:35:48 +01:00
Hugo Osvaldo Barrera
45b67122fe Fast-mail publishing if credentials are missing 2023-12-18 14:35:48 +01:00
Jason Cox
7a387b8efe Require matching BEGIN and END lines in vobjects
Raise an error when parsing a vobject that has mismatched `BEGIN` and
`END` lines (e.g., `BEGIN:FOO` followed by `END:BAR`) or missing `END`
lines (e.g., `BEGIN:FOO` with no subsequent `END:FOO`).

Fixes #1102.
2023-12-13 10:31:32 -05:00
Kai Herlemann
889e1f9ea2 Implement a no_delete flag
See: https://github.com/pimutils/vdirsyncer/pull/1090
2023-11-29 23:50:32 +08:00
azrdev
d1f93ea0be docs: add instructions to get pw from environment variable
tested with vdirsyncer 0.19.2 on archlinux
2023-11-26 08:20:20 +01:00
Hugo Osvaldo Barrera
82fd03be64 Clarify that pipx won't install man pages
And reword the section a bit.
2023-11-16 07:19:15 +08:00
Hugo Osvaldo Barrera
b50f9def00 Ensure type annotations are backwards compatible
Related: https://github.com/pimutils/todoman/issues/544
2023-10-29 16:04:23 +01:00
Hugo Osvaldo Barrera
91c16b3215 Add a changelog entry for vcard 4.0 support 2023-10-07 03:26:04 +02:00
wrvsrx
d45ae04006 Update doc about use_vcard_4 2023-10-06 23:18:26 +02:00
wrvsrx
9abf9c8e45 Add an option to use vCard 4.0
Fix #503
2023-10-06 23:18:21 +02:00
Hugo Osvaldo Barrera
0f0e5b97d3 Ignore type checking lines in coverage report 2023-09-25 16:24:52 +02:00
Hugo Osvaldo Barrera
301aa0e16f pre-commit run --all 2023-09-24 12:41:56 +02:00
Hugo Osvaldo Barrera
dcd3b7a359 pre-commit autoupdate 2023-09-24 12:35:40 +02:00
Hugo Osvaldo Barrera
df8c4a1cf5 pre-commit: fix ruff hook being a no-op 2023-09-24 12:35:16 +02:00
suiso67
5a17ec1bba Fix wrong document formatting 2023-09-08 13:03:00 +02:00
suiso67
ab3aa108fc Fix broken Arch Linux package link 2023-09-08 13:03:00 +02:00
Justin !
f194bb0a4c Do not allow None value if we assert they're not None on the next line
This change imply changing the `save_status` parameters order. If you
don't like that, I can drop this commit.
2023-08-23 16:20:21 +02:00
Justin !
c073d55b2f Don't allow load_status to return None 2023-08-23 16:20:21 +02:00
Justin !
3611e7d62f Add type hint to vdirsyncer/cli/utils.py 2023-08-23 16:20:21 +02:00
Jan Moeller
adc974bdd1 docs: add changelog for #1081 2023-08-06 12:45:42 +02:00
Jan Moeller
efad9eb624 fix(repair_collection): use DiscoverResult logic to discover collection
fixes error: `DAVSession.__init__() missing 1 required keyword-only argument:
'connector'`.
Reuses the existing logic in DiscoverResult to determine if the storage requires
a 'connector' arg.
2023-08-06 12:45:42 +02:00
pre-commit-ci[bot]
246568f149 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-07-14 11:55:08 +02:00
Justin !
439f1e6f50 Run ruff --fix vdirsyncer 2023-07-14 11:55:08 +02:00
Justin !
ef8e8980d1 Add Typing annotation to cli/config.py 2023-07-14 11:55:08 +02:00
Justin !
08616abbb5 Add typing annotation to cli/__init__.py 2023-07-14 11:55:08 +02:00
Hugo Osvaldo Barrera
4237ff863c Slightly improve docs for configuring google 2023-07-13 12:34:57 +02:00
Hugo Osvaldo Barrera
1a6ad54543 ci: Standardise jobs a bit 2023-07-13 12:33:58 +02:00
Claudio Noguera
203468fd25 Update config.rst
This is the way it actually works.
With web application, a 400 is returned. With desktop it works fine
2023-07-13 12:32:00 +02:00
Hugo Osvaldo Barrera
6368af1365 ci: remove superfluous export
We're now using a virtualenv.
2023-06-26 19:25:53 +02:00
Hugo Osvaldo Barrera
b38306bdd0 ci: Ensure that minimal job runs on older Python
Fixes: https://github.com/pimutils/vdirsyncer/issues/1077
2023-06-26 19:25:40 +02:00
Hugo Osvaldo Barrera
d26557bee3 Python 3.10 and 3.11 are also supported
We've been running 3.11 on CI for a while now.
2023-06-26 19:04:23 +02:00
Hugo Osvaldo Barrera
b9f749467c Add forward-compatibility for storage type parameter
The Rust rewrite of vdirsyncer requires explicitly specifying what type
of "filesystem" storage is being used. These can be either
"filesystem/icalendar" or "filesystem/vcard".

Add forward-compatibility with this upcoming format, by allowing (but
ignoring) a slash and anything after it.

This makes configuration files that have been updated for the Rust
implementation compatible with the Python implementation.

Closes: https://github.com/pimutils/vdirsyncer/pull/1075
2023-06-26 19:01:52 +02:00
Hugo Osvaldo Barrera
7e5910a341 ci: use virtualenvs for jobs that use pip
Pip now refuses to tamper with the system python installation.
2023-06-26 19:00:32 +02:00
Hugo Osvaldo Barrera
7403182645 Update changelog with recent updates 2023-06-26 18:50:00 +02:00
Henning Sudbrock
bad381e5ba Fix link to GNU Guix package in documentation
Fixes #1071
2023-05-21 10:47:07 +02:00
pre-commit-ci[bot]
700586d959 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
- [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.2.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.249 → v0.0.265](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.249...v0.0.265)
2023-05-09 11:52:48 +02:00
Hugo Osvaldo Barrera
c1d3efb6b8 Make broken test as xfail 2023-05-09 11:52:48 +02:00
Hugo Osvaldo Barrera
c55b969791 Add some type hints 2023-05-09 11:52:48 +02:00
Florian Preinstorfer
079a156bf8 Remove superflous string quotes 2023-03-30 14:03:32 +02:00
Hugo Osvaldo Barrera
242216d85a Brain typo 2023-03-28 23:23:44 +02:00
Hugo Osvaldo Barrera
b1ef68089b Properly populate cache during SingleFileStorage._at_once
The call to `list` was never awaited and the stream never drained, so
the cache remained empty.
2023-03-10 12:27:48 +01:00
Enrico Guiraud
85ae33955f
Prevent single file storage from performing unnecessary N^2 loop
For single file storage we wrap the logic of get_multi with the
at_once context manager so that `self.list()` (which is called by
`self.get()`) actually caches the items rather than re-computing
them at every call.

This should largely mitigate the performance issue describe at
https://github.com/pimutils/vdirsyncer/issues/818 . The issue
discussion also contains more background about this patch.
2023-03-09 17:59:20 -06:00
Timo Ludwig
54a90aa5dd Document caveats of Google contacts storage
- Group labels are not mapped to CATEGORIES property
- BDAY property is missing when date is incomplete
2023-03-06 09:57:00 +01:00
Hugo Osvaldo Barrera
443ae3d3e7 Fix crash when using auth certs
Fixes: https://github.com/pimutils/vdirsyncer/issues/1033
2023-02-28 16:21:28 +01:00
pre-commit-ci[bot]
3bf9a3d684 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v1.0.0 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.0...v1.0.1)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.246 → v0.0.249](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.246...v0.0.249)
2023-02-21 10:57:28 +01:00
Hugo Osvaldo Barrera
2138c43456 Update docs for Google storages
References: https://github.com/pimutils/vdirsyncer/pull/985
References: https://github.com/pimutils/vdirsyncer/issues/975
Closes: https://github.com/pimutils/vdirsyncer/issues/1028
Closes: https://github.com/pimutils/vdirsyncer/issues/808
2023-02-16 23:17:27 +01:00
Hugo Osvaldo Barrera
5a46c93987 mypy: Drop unnecessary rule exclusion 2023-02-16 16:51:12 +01:00
Hugo Osvaldo Barrera
180f91f0fe Move mypy config to pyproject.toml 2023-02-16 16:51:12 +01:00
Hugo Osvaldo Barrera
6443d37c97 Move pytest config to pyproject.toml 2023-02-16 16:51:12 +01:00
pre-commit-ci[bot]
13ca008380 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.0](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.243 → v0.0.246](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.243...v0.0.246)
2023-02-14 10:09:36 +01:00
Hugo Osvaldo Barrera
24cb49f64c Remove superfluous exception parens 2023-02-10 16:57:39 +01:00
Hugo Osvaldo Barrera
defe8e2591 Fix broken BSD link
pkgsrc.se is no more.
2023-02-10 16:54:08 +01:00
pre-commit-ci[bot]
e11fa357ff [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-02-07 12:21:17 +01:00
pre-commit-ci[bot]
e20a65793e [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.238 → v0.0.243](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.238...v0.0.243)
2023-02-07 12:21:17 +01:00
pre-commit-ci[bot]
df14865f43 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.237 → v0.0.238](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.237...v0.0.238)
2023-01-31 10:09:31 +01:00
Hugo Osvaldo Barrera
f45ecf6ad0 Simplify management of documentation dependencies 2023-01-31 01:11:24 +01:00
Hugo Osvaldo Barrera
72bcef282d Remove another unnecessary wrapper 2023-01-31 01:11:24 +01:00
Hugo Osvaldo Barrera
3a56f26d05 Drop unnecessary wrapping 2023-01-31 01:11:24 +01:00
Hugo Osvaldo Barrera
4dd17c7f59 This make target is gone; use pytest directly 2023-01-31 01:11:04 +01:00
Hugo Osvaldo Barrera
73f2554932 Check typos via pre-commit
So this runs more often (and also in CI).
2023-01-31 01:11:04 +01:00
Hugo Osvaldo Barrera
627f574777 Remove unnecessary param 2023-01-31 01:11:04 +01:00
Hugo Osvaldo Barrera
37a7f9bea8 This test is not async 2023-01-31 00:21:04 +01:00
Hugo Osvaldo Barrera
d2d1532883 Remove indirection 2023-01-31 00:21:04 +01:00
Hugo Osvaldo Barrera
0dcef26b9d Update build jobs with more descriptive names 2023-01-31 00:21:04 +01:00
Hugo Osvaldo Barrera
d646357cd3 Use ruff for code checking and linting
`isort` is failing in pre-commit.ci right now, so this might be good
timing.

See: https://whynothugo.nl/journal/2023/01/20/notes-on-ruff/
2023-01-31 00:19:14 +01:00
Hugo Osvaldo Barrera
8c6c0be15a This mutation of the control variable is intended 2023-01-31 00:19:14 +01:00
Hugo Osvaldo Barrera
dfc29db312 Use dict literal instead of dict() call 2023-01-31 00:19:14 +01:00
Hugo Osvaldo Barrera
a41cf64b6c Update ArchLinux CI setup 2023-01-31 00:19:14 +01:00
Hugo Osvaldo Barrera
a2eda52b71 Hottub expect DOS-style extensions 2023-01-31 00:19:14 +01:00
Hugo Osvaldo Barrera
61006f0685 Improve installation documentation
`pipx` is a lot simpler on any setup where this it is available.
2023-01-26 18:53:19 +01:00
Hugo Osvaldo Barrera
9b48bccde2 Fix return type
Fixes: https://github.com/pimutils/vdirsyncer/issues/1036
2023-01-26 18:01:08 +01:00
Hugo Osvaldo Barrera
7c72caef3f docs: We're not using aiohttp, not requests 2023-01-26 10:43:46 +01:00
Hugo Osvaldo Barrera
0045b23800 Add missing changelog entry
See: https://github.com/pimutils/vdirsyncer/pull/1031
2023-01-13 16:37:32 +01:00
Hugo Osvaldo Barrera
c07fbc2053 Add missing changelog entry
See: https://github.com/pimutils/vdirsyncer/pull/1016
2023-01-13 16:36:45 +01:00
Daniele Ricci
e3485beb45 Enable environment variables for HTTP proxy 2023-01-13 16:36:36 +01:00
Tonus
0f83fd96d5 Add Slackware as build-able version
I maintain the build script for Slackware on the slackbuilds.org repo (endorsed by Slackware).
2023-01-07 01:39:12 +01:00
chrysle
8980a80560 Corrected installation steps for Ubuntu and pip 2023-01-06 14:50:27 +01:00
pre-commit-ci[bot]
90b6ce1d04 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.2.2 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.2...v3.3.0)
2022-12-09 12:01:43 +01:00
waclaw66
7a801d3d5d Google Contacts discover fix 2022-12-09 12:00:52 +01:00
Hugo Osvaldo Barrera
2c44f7d773 Update flake8 comments to "new" format 2022-12-03 16:25:08 +01:00
Hugo Osvaldo Barrera
6506c86f58 Remove obsolete config value
This was used by flake8-import-order
2022-12-03 16:25:08 +01:00
pre-commit-ci[bot]
51b409017d [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
- [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0)
- [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991)
2022-12-03 16:25:08 +01:00
Hugo Osvaldo Barrera
84613e73b0 Split out publishing for usual CI job
This separate one is to be triggered manually for tags.

Keep it simple.
2022-11-20 15:30:21 +01:00
Hugo Osvaldo Barrera
a4ef45095e Tidy up changelog for v0.19.beta1 2022-11-19 15:25:03 +01:00
Hugo Osvaldo Barrera
63ba948241 Fix mistaken return type
This return value is not used anywhere (clearly).
2022-11-19 15:17:11 +01:00
pre-commit-ci[bot]
3067b32de5 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.6.0 → 22.10.0](https://github.com/psf/black/compare/22.6.0...22.10.0)
- [github.com/asottile/pyupgrade: v2.37.3 → v3.2.2](https://github.com/asottile/pyupgrade/compare/v2.37.3...v3.2.2)
- [github.com/pre-commit/mirrors-mypy: v0.971 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.971...v0.990)
2022-11-19 15:17:11 +01:00
Hugo Osvaldo Barrera
a87518c474 Fix weird string formatting 2022-11-07 17:21:51 +01:00
Hugo Osvaldo Barrera
b26e771865 Fix bad syntax for adding finalizers 2022-11-07 17:21:10 +01:00
Hugo Osvaldo Barrera
2fbb0ab7a5 Clean up some invalid TLS configuration branches 2022-09-20 23:01:50 +02:00
Hugo Osvaldo Barrera
60352f84fe Untangle auth handling
This was a bit entangled and messed up due to recent changes.
2022-09-20 23:01:50 +02:00
Hugo Osvaldo Barrera
b7201013bc Remove duplicate command 2022-09-20 23:01:50 +02:00
Hugo Osvaldo Barrera
b61095ad47 Async fixtures must be marked as such
pytest_asyncio now uses strict mode by default.
2022-09-20 23:01:47 +02:00
pre-commit-ci[bot]
278e6de8b0 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 4.0.1 → 5.0.4](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.4)
- [github.com/asottile/pyupgrade: v2.34.0 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.3)
- [github.com/pre-commit/mirrors-mypy: v0.961 → v0.971](https://github.com/pre-commit/mirrors-mypy/compare/v0.961...v0.971)
2022-08-09 21:11:42 +02:00
Hugo Osvaldo Barrera
843c58b92e Ignore flake8-bugbear false positive
See: https://github.com/PyCQA/flake8-bugbear/issues/269
2022-08-09 21:10:17 +02:00
Hugo Osvaldo Barrera
cd412aa161 Rename master branch to main 2022-08-05 17:07:33 +02:00
rEnr3n
c5f80d1644
Make systemd service restart only once on failure
Fixes https://github.com/pimutils/vdirsyncer/issues/998
2022-08-05 21:28:54 +08:00
Hugo
c50eabc77e
Merge pull request #995 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-07-05 09:04:08 +00:00
pre-commit-ci[bot]
a88389c4f1
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0)
2022-07-04 22:14:46 +00:00
Hugo
1f7497c9d1
Merge pull request #985 from telotortium/gcal-oauth-remove-oob
gcal: replace oob OAuth2 with local server redirect
2022-06-26 18:42:37 +00:00
robert.irelan
baaf737873 gcal: replace oob OAuth2 with local server redirect
Google Calendar has disabled the oob method for new credentials (see
https://developers.google.com/identity/protocols/oauth2/native-app), so
new users cannot currently use Google Calendar. Fix this by switching to
a loopback redirect_uri flow instead.

Co-authored-by: Hugo Osvaldo Barrera <hugo@whynothugo.nl>
2022-06-26 19:40:35 +02:00
Hugo Osvaldo Barrera
7c2fed1ceb Update homebrew references
Also, it's called "macOS" nowadays.
2022-06-21 14:16:41 +02:00
Hugo Osvaldo Barrera
3be048be18 Drop macOS plist
This file is now generated by homebrew for mac users. As we have no
macOS devices to maintain our own, we'll just rely on their
implementation.

See: dfd51bebed/Formula/vdirsyncer.rb (L87-L94)
See: https://github.com/pimutils/vdirsyncer/pull/978
2022-06-21 14:16:41 +02:00
pre-commit-ci[bot]
f103b10b2a [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0)
- [github.com/asottile/pyupgrade: v2.32.1 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.34.0)
- [github.com/pre-commit/mirrors-mypy: v0.960 → v0.961](https://github.com/pre-commit/mirrors-mypy/compare/v0.960...v0.961)
2022-06-14 09:54:00 +02:00
Hugo
e44c704ae3
Merge pull request #987 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-05-30 22:58:36 +02:00
pre-commit-ci[bot]
f32e0a9c1f
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.950 → v0.960](https://github.com/pre-commit/mirrors-mypy/compare/v0.950...v0.960)
2022-05-30 20:43:36 +00:00
Hugo
24e3625cc0
Merge pull request #982 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-05-09 23:05:26 +02:00
pre-commit-ci[bot]
4df54b9231
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.32.0 → v2.32.1](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.32.1)
2022-05-09 20:38:13 +00:00
Hugo
8557c6e0bb
Merge pull request #981 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-05-02 23:06:13 +02:00
pre-commit-ci[bot]
9fdc93c140
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.942 → v0.950](https://github.com/pre-commit/mirrors-mypy/compare/v0.942...v0.950)
2022-05-02 20:29:45 +00:00
Hugo
f3f8eb6824
Merge pull request #976 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-04-12 12:06:26 +02:00
pre-commit-ci[bot]
b18e1c78d2 Auto fixes from pre-commit hooks 2022-04-12 00:03:51 +02:00
pre-commit-ci[bot]
0a4114ef9f
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0)
- [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0)
2022-04-11 21:09:08 +00:00
Hugo
06f8001d65
Merge pull request #974 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-04-05 09:36:21 +02:00
pre-commit-ci[bot]
61f3785e6c
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.1.0 → 22.3.0](https://github.com/psf/black/compare/22.1.0...22.3.0)
2022-04-04 20:08:40 +00:00
Hugo
b0020f9436
Merge pull request #973 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-03-28 21:40:48 +02:00
pre-commit-ci[bot]
74d738ec80
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.941 → v0.942](https://github.com/pre-commit/mirrors-mypy/compare/v0.941...v0.942)
2022-03-28 19:22:10 +00:00
Hugo
711eccedab
Merge pull request #972 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-03-22 13:24:40 +01:00
Hugo Osvaldo Barrera
5d58a86ba0 Avoid shadowing iterable inside for loop 2022-03-22 08:27:08 +01:00
pre-commit-ci[bot]
60c3b59552 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-03-21 22:41:54 +00:00
pre-commit-ci[bot]
22a127191d
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.12b0 → 22.1.0](https://github.com/psf/black/compare/21.12b0...22.1.0)
- [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1)
- [github.com/pre-commit/mirrors-mypy: v0.931 → v0.941](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v0.941)
2022-03-21 22:41:09 +00:00
Hugo
dc6e4ba5af
Merge pull request #971 from pimutils/update-click-log
Support click-log 0.4.0
2022-03-15 19:55:58 +01:00
Hugo Osvaldo Barrera
ea640001d0 Support click-log 0.4.0 2022-03-14 16:25:54 +01:00
Hugo Osvaldo Barrera
545b8ce2f1 token_updater needs to be an async function 2022-02-06 00:55:31 +01:00
Hugo Osvaldo Barrera
3035d9cfae Add some extra type hints 2022-02-06 00:55:17 +01:00
Hugo Osvaldo Barrera
68c5968be8 Tidy up spacing in donations page 2022-01-16 05:03:52 +01:00
Hugo Osvaldo Barrera
0d1ca319c0
Merge pull request #962 from pimutils/donations
Update donations links
2022-01-16 05:02:44 +01:00
Hugo Osvaldo Barrera
817eab51f1
Update donations links
Signed-off-by: Hugo Osvaldo Barrera <hugo@barrera.io>
2022-01-15 20:15:05 +01:00
Hugo Osvaldo Barrera
e8b72130c2
Merge pull request #961 from dilyanpalauzov/readonly_metadata
vdirsyncer/metasync.py: for read_only storages adjust the metadata resolution in favour of the read_only storage
2022-01-14 16:39:23 +01:00
Дилян Палаузов
8a44b278d1 vdirsyncer/metasync.py: for read_only storages adjust the metadata resolution in favour of the read_only storage 2022-01-14 13:52:04 +02:00
Hugo Osvaldo Barrera
54a5bf4ad3
Merge pull request #953 from electrickite/shell-fetch
Add shell strategy to fetch params
2022-01-14 12:48:07 +01:00
Hugo Osvaldo Barrera
10659b80ba
Merge pull request #960 from dilyanpalauzov/readonly_metadata
storage/base: for read_only storages do not overwrite the meta data
2022-01-14 12:47:18 +01:00
Дилян Палаузов
1c6beae9b4 storage/base: for read_only storages do not overwrite the meta data 2022-01-14 13:10:49 +02:00
Hugo Osvaldo Barrera
7ce9466c46
Merge pull request #959 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-01-10 22:31:17 +01:00
pre-commit-ci[bot]
9f0390ee21
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.930 → v0.931](https://github.com/pre-commit/mirrors-mypy/compare/v0.930...v0.931)
2022-01-10 20:32:13 +00:00
Hugo Osvaldo Barrera
4e3f39468b
Merge pull request #958 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-01-04 16:03:08 +01:00
pre-commit-ci[bot]
a7e984f013
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0)
2022-01-03 20:09:57 +00:00
Hugo Osvaldo Barrera
7c7f97c6b2
Merge pull request #956 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-12-27 22:30:22 +01:00
pre-commit-ci[bot]
7e9132b817
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0)
- [github.com/pre-commit/mirrors-mypy: v0.910-1 → v0.930](https://github.com/pre-commit/mirrors-mypy/compare/v0.910-1...v0.930)
2021-12-27 20:15:19 +00:00
pre-commit-ci[bot]
59b95d9999 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-12-09 16:18:10 +00:00
Corey Hinshaw
4dd15716db Adds shell strategy to config fetch params to run command string in a shell 2021-12-09 11:17:43 -05:00
Hugo Osvaldo Barrera
ec101b20d6
Merge pull request #947 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-12-06 21:11:35 +00:00
pre-commit-ci[bot]
2c551afafb [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-12-06 20:14:52 +00:00
pre-commit-ci[bot]
ad7bb82f40
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.9b0 → 21.12b0](https://github.com/psf/black/compare/21.9b0...21.12b0)
- [github.com/pycqa/isort: 5.9.3 → 5.10.1](https://github.com/pycqa/isort/compare/5.9.3...5.10.1)
- [github.com/asottile/pyupgrade: v2.29.0 → v2.29.1](https://github.com/asottile/pyupgrade/compare/v2.29.0...v2.29.1)
2021-12-06 20:13:26 +00:00
Hugo Osvaldo Barrera
961203e865 Require aiohttp >= 3.8.0
See https://github.com/pimutils/vdirsyncer/issues/916#issuecomment-957671206
See https://github.com/aio-libs/aiohttp/issues/5156
2021-11-02 18:07:13 +01:00
Hugo Osvaldo Barrera
d72536805c
Merge pull request #943 from pimutils/deb
Update script to publish deb packages
2021-10-22 20:15:05 +02:00
Hugo Osvaldo Barrera
ac6e19261f
Merge pull request #944 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-10-11 23:29:08 +02:00
pre-commit-ci[bot]
cbb0cad827
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1)
- [github.com/pre-commit/mirrors-mypy: v0.910 → v0.910-1](https://github.com/pre-commit/mirrors-mypy/compare/v0.910...v0.910-1)
2021-10-11 19:00:13 +00:00
Hugo Osvaldo Barrera
2c69f865f0 Update script to publish deb packages
Creating a docker image and then running inside of it was a bit complex,
and tricky to debug when things needed maintenance.

Just use a debian/ubuntu container, but run our script inside of it.
Manually debugging is much easier, and the whole setup is a bit simpler.
2021-10-06 00:48:25 +02:00
Hugo Osvaldo Barrera
63510414ae
Merge pull request #941 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-10-05 22:38:00 +02:00
Hugo Osvaldo Barrera
fce5062a12
Merge pull request #942 from pimutils/drop-etesync
Drop etesync
2021-10-05 22:26:35 +02:00
Hugo Osvaldo Barrera
c77d750ef6 Drop etesync
The current [experimental] implementation is stale, and hasn't been
maintained for a long time. Regrettably, not even its tests still work,
and there nobody interested in maintaining it.

If anyone is interested in re-implementing this in a third-party
package, I can consider adding support for pluggable storages.
2021-10-05 22:13:59 +02:00
pre-commit-ci[bot]
02ee9f96e4
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.28.0 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.28.0...v2.29.0)
2021-10-04 19:00:20 +00:00
Hugo Osvaldo Barrera
ddaeccb2ee
Merge pull request #940 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-27 22:00:46 +02:00
pre-commit-ci[bot]
63ef204835 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-09-27 18:57:05 +00:00
pre-commit-ci[bot]
7d61cd3e2e
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.26.0 → v2.28.0](https://github.com/asottile/pyupgrade/compare/v2.26.0...v2.28.0)
2021-09-27 18:56:47 +00:00
Hugo Osvaldo Barrera
db6da70c26
Merge pull request #939 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-22 21:02:10 +02:00
pre-commit-ci[bot]
bf95bf2941
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.8b0 → 21.9b0](https://github.com/psf/black/compare/21.8b0...21.9b0)
2021-09-20 19:07:52 +00:00
Hugo Osvaldo Barrera
b3c9df1b1d
Merge pull request #937 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-13 21:42:01 +02:00
Hugo Osvaldo Barrera
87574be547 Stop running pre-commit twice 2021-09-13 21:27:11 +02:00
Hugo Osvaldo Barrera
2e35214421 Remove unused import 2021-09-13 21:27:06 +02:00
pre-commit-ci[bot]
f5c2026dcf [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-09-13 18:57:16 +00:00
pre-commit-ci[bot]
acf29cf659
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.25.0 → v2.26.0](https://github.com/asottile/pyupgrade/compare/v2.25.0...v2.26.0)
2021-09-13 18:56:33 +00:00
Hugo Osvaldo Barrera
fbd5ff88d5
Merge pull request #935 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-08-30 22:23:53 +02:00
pre-commit-ci[bot]
7605416054
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.7b0 → 21.8b0](https://github.com/psf/black/compare/21.7b0...21.8b0)
- [github.com/asottile/pyupgrade: v2.24.0 → v2.25.0](https://github.com/asottile/pyupgrade/compare/v2.24.0...v2.25.0)
2021-08-30 18:41:20 +00:00
Hugo Osvaldo Barrera
7a12e6028c
Merge pull request #933 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-08-23 20:39:58 +02:00
pre-commit-ci[bot]
d6876c6bad
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.23.3 → v2.24.0](https://github.com/asottile/pyupgrade/compare/v2.23.3...v2.24.0)
2021-08-23 18:16:49 +00:00
Hugo Osvaldo Barrera
09eb375c5b
Merge pull request #929 from pimutils/fix-918
Fix 918
2021-08-21 15:39:05 +02:00
Hugo Osvaldo Barrera
48747463ed Remove unused code 2021-08-19 20:40:47 +02:00
Hugo Osvaldo Barrera
955f434d9d Also test syncing with an href with a colon
Since this was the actual problematic character.
2021-08-18 18:41:00 +02:00
Hugo Osvaldo Barrera
889183ec89 I think this makes sense 2021-08-18 18:20:04 +02:00
Hugo Osvaldo Barrera
0650cc3bc2 Add test for #918
We're doing something wrong with UID/href quoting/unquoting, but I've
yet to figure out what.
2021-08-18 18:20:04 +02:00
Hugo Osvaldo Barrera
6281e7a237 Radicale now passes for this test 2021-08-18 18:20:04 +02:00
Hugo Osvaldo Barrera
dff48f101b
Merge pull request #928 from pimutils/fastmail-recurrence-sorting
Work around quirk in Fastmail
2021-08-18 18:19:35 +02:00
Hugo Osvaldo Barrera
1081a15895 Work around quirk in Fastmail
They seem to sort the parameters in an RRULE in a specific order, so
just use that by default to avoid any mismatch.
2021-08-16 22:29:16 +02:00
Hugo Osvaldo Barrera
cf1d082628 Use context managers for aio connectors
Not sure why we didn't do this initially, but this ensures that we
always close all connectors properly, and also gives much clearer scope
regarding their life-cycles.
2021-08-16 21:40:11 +02:00
Hugo Osvaldo Barrera
54e829262d
Merge pull request #926 from pimutils/python37-tests
Run tests CI python 3.7
2021-08-07 23:29:13 +02:00
Hugo Osvaldo Barrera
8830307e38 Drop syntax that won't run on Python 3.7 2021-08-07 17:26:01 +02:00
Hugo Osvaldo Barrera
7a7deffa2c Run test on Python 3.7
Run these on a synthetic environment, since no distribution seems to
currently ship this version.
2021-08-07 17:23:10 +02:00
Hugo Osvaldo Barrera
ecb181d9d7
Merge pull request #925 from pimutils/pyupgrade
Set up pyupgrade
2021-08-04 20:43:50 +02:00
Hugo Osvaldo Barrera
fed1ee69c3 Run pyupgrade 2021-08-04 19:58:59 +02:00
Hugo Osvaldo Barrera
48aa4912a2 Add pyupgrade as a pre-commit hook 2021-08-04 19:58:31 +02:00
Hugo Osvaldo Barrera
8886854367
Merge pull request #912 from pimutils/typing
Add some typing hints
2021-08-04 15:12:35 +02:00
Hugo Osvaldo Barrera
a910e9f446 Stop marking wheels as universal
We don't support Python 2, so they're not universal.
2021-08-04 00:18:22 +02:00
Hugo Osvaldo Barrera
f3714fc493 Add type hints and configure mypy
Configure mypy as a pre-commit hook and add all type hints necessary for
mypy to pass.

There's still more work to be done here typing a lot more code, but this
provides a clear starting point.
2021-08-03 19:23:37 +02:00
Hugo Osvaldo Barrera
6af4dd124b
Merge pull request #924 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-08-02 22:09:04 +02:00
pre-commit-ci[bot]
bc5e03630e
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pycqa/isort: 5.9.2 → 5.9.3](https://github.com/pycqa/isort/compare/5.9.2...5.9.3)
2021-08-02 18:06:08 +00:00
Hugo Osvaldo Barrera
6491bc53fb
Merge pull request #923 from pimutils/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-07-27 18:26:21 +02:00
pre-commit-ci[bot]
21eccfc2ef
[pre-commit.ci] pre-commit autoupdate
updates:
- https://gitlab.com/pycqa/flake8https://github.com/PyCQA/flake8
- [github.com/psf/black: 21.6b0 → 21.7b0](https://github.com/psf/black/compare/21.6b0...21.7b0)
- [github.com/pycqa/isort: 5.8.0 → 5.9.2](https://github.com/pycqa/isort/compare/5.8.0...5.9.2)
2021-07-26 17:54:50 +00:00
Hugo Osvaldo Barrera
7b493416f7
Merge pull request #920 from pimutils/meta_delete
metasync: use None as no-value and delete missing values on syncing
2021-07-26 13:07:45 +02:00
Hugo Osvaldo Barrera
5b8f00e720 Sort imports 2021-07-25 18:35:47 +02:00
Hugo Osvaldo Barrera
0556e53e0c Use isort to reorder python imports
reorder-python-imports is hard to set up correctly.

isort is a lot simpler to set to set up it locally, and is kinda
becoming the standard in the python world.

The import style remains the same, and is tweaked to minimise conflicts.
2021-07-25 18:35:04 +02:00
Hugo Osvaldo Barrera
7379a96f73 Skip test on unsupported servers 2021-07-23 20:27:34 +02:00
Дилян Палаузов
c0ccb3d1aa metasync: use None as no-value and delete missing values on syncing
- change the interface of Storage.get_meta() and .set_meta()
  to use '' as the empty value and None as missing value.

- When a property is missing (e.g calendar-color was removed)
  in the filesystem storage delete the 'color' file, and
  in the WebDAV storage issue propertyupdate/remove call.

- remove the property from [status]/[pair]/[collection].metadata

I have adjusted, but not run the test suite.  On the other side
I tested on real system: source is CalDAV - destination is filesystem;
and source is CalDAV, destination is CalDAV (another account)
2021-07-23 19:07:02 +02:00
Hugo Osvaldo Barrera
98fcd12fa7 Only measure coverage of vdirsyncer/
i.e.: Ignore non-application code like  contrib/
2021-07-10 17:35:50 +02:00
Hugo Osvaldo Barrera
f2a1afe6d3
Merge pull request #915 from pimutils/formatting
Make command bit more readable
2021-07-10 17:33:34 +02:00
Hugo Osvaldo Barrera
e16f83c1c2 Make this bit more readable 2021-07-07 18:46:13 +02:00
Hugo Osvaldo Barrera
55c563ff8c
Merge pull request #893 from Intevation/conflict-resolution-interactive
Add contrib script to resolve conflicts
2021-07-07 18:41:54 +02:00
Hugo Osvaldo Barrera
a5731b269e
Merge pull request #913 from pimutils/async-collections
Sync pairs asynchronously
2021-07-07 18:21:06 +02:00
Hugo Osvaldo Barrera
459efbf7af
Merge pull request #914 from pimutils/simpler-coverage
Simplify coverage submission
2021-07-07 15:10:10 +02:00
Hugo Osvaldo Barrera
58aa0a3a31 Simplify coverage submission
We submit separate coverage reports for each group of tests, but codecov
doesn't give us proper tooling to analyse each one separately. When
there's a percentage drop in one, there's no way to pinpoint where -- at
least not without running locally and inspecting results separately.

Treat all coverage as one. Analysis of coverage for each group of tests
can be done offline and manually, but there's little value in keeping it
in codecov.

Should also help us transition a more simplified CI design (e.g.:
running all tests together rather than in groups).
2021-07-06 23:45:35 +02:00
Hugo Osvaldo Barrera
177748d3d1 Sync pairs asynchronously 2021-07-06 23:13:13 +02:00
Bernhard Reiter
61edfc090e
Adjust codestyle
running flake8, black and reorder-python-imports.
2021-06-28 16:01:21 +02:00
Bernhard Reiter
b3bee77c17
Change license from Apache-2.0 to BSD-3-Clause
to match the license of vdirsyncer
2021-06-28 15:22:56 +02:00
Hugo Osvaldo Barrera
21db2547cb
Merge pull request #911 from pimutils/optimise-test-servers
Minor optimisations to tests
2021-06-27 18:31:11 +02:00
Hugo Osvaldo Barrera
be131a0063
Merge pull request #910 from pimutils/orage
Update link for orage
2021-06-27 13:17:06 +02:00
Hugo Osvaldo Barrera
71879045e4 Tidy up test collection creation
- No need to empty collections, they're generated with a UUID and should
  always be empty (the code to empty them does not actually run in CI
  right now).
- Move deletion code to _after_ the yield, since that's the order in
  which things happen.
- Delete all collections asynchronously.
2021-06-27 13:16:31 +02:00
Hugo Osvaldo Barrera
54e0c114fa Update link for orage
The previous one is 404, and that domain redirects to some random
website.
2021-06-27 12:37:33 +02:00
Hugo Osvaldo Barrera
17f422c1b7 Auto-delete test containers 2021-06-26 19:59:56 +02:00
Hugo Osvaldo Barrera
a9f1a5195a Tweak output when running tests 2021-06-26 19:59:56 +02:00
Hugo Osvaldo Barrera
8dab258ef0
Merge pull request #909 from pimutils/fastmail
Run Fastmail on CI again
2021-06-26 18:40:50 +02:00
Hugo Osvaldo Barrera
f09d060580 Run Fastmail tests on CI 2021-06-26 18:25:35 +02:00
Hugo Osvaldo Barrera
ef2419efa9 Avoid running bogus tests with Fastmail
The fix that disabled VTODO tests accidentally ran all other tests twice
for this storage.

This wasn't picked up earlier since Fastmail is not currently running on
CI.
2021-06-26 18:12:54 +02:00
Hugo Osvaldo Barrera
2eff8e08e1 Fix breakage in Fastmail tests
Some code that wasn't updated with the switch to asyncio.
2021-06-26 18:12:22 +02:00
Hugo Osvaldo Barrera
152ebb05dd
Merge pull request #906 from pimutils/async
Initial asyncio support
2021-06-26 15:56:23 +02:00
Hugo Osvaldo Barrera
5a9fc2cc7e Add changelog entries for asyncio support 2021-06-26 13:40:38 +02:00
Hugo Osvaldo Barrera
dfed9794cb Port google storage to use asyncio 2021-06-26 13:40:38 +02:00
Hugo Osvaldo Barrera
8d69b73c9e Fetch displaynames asynchronously 2021-06-26 13:40:38 +02:00
Hugo Osvaldo Barrera
1a1f6f0788 Initial async support
Add asyncio to the storage backends and most of the codebase. A lot of
it merely uses asyncio APIs, but still doesn't actually run several
things concurrently internally. Further improvements will be added on
top of these changes

Thanks to  Thomas Grainger (@graingert) for a few useful pointers
related to asyncio.
2021-06-26 13:40:35 +02:00
Hugo Osvaldo Barrera
7c9170c677 Remove unecessary indirection 2021-06-26 13:02:41 +02:00
Hugo Osvaldo Barrera
623c0537e1 Update test 2021-06-26 13:02:41 +02:00
Hugo Osvaldo Barrera
4930b5f389 Drop multithreading support
This is mainly in preparation to moving to an async architecture.
2021-06-26 13:02:41 +02:00
Hugo Osvaldo Barrera
25435ce11d
Merge pull request #903 from pimutils/fix-old-ssl-tests
Fix SSL tests failing due to old weak MDs
2021-06-26 13:01:17 +02:00
Hugo Osvaldo Barrera
1f6cc6f8be Fix SSL tests failing due to old weak MDs
Had to drop pytest-localserver, since it's broken and upstream is gone.
2021-06-20 18:49:02 +02:00
Hugo Osvaldo Barrera
59b6e24795
Merge pull request #905 from pimutils/showconfig
Add a command to print the current config
2021-06-19 13:38:15 +02:00
Hugo Osvaldo Barrera
722dace828
Merge pull request #904 from pimutils/improve-error-msg
Fix double-use of a generator
2021-06-19 13:11:24 +02:00
Hugo Osvaldo Barrera
6cebba0853 Add a changelog entry 2021-06-17 08:42:45 +02:00
Hugo Osvaldo Barrera
29528123a3 Add a command to print the current config
This is intended to be used by external tools and integrations (such as
daemons which trigger `vdirsyncer` when there's a change in a
collection).
2021-06-16 19:03:50 +02:00
Hugo Osvaldo Barrera
72618e374d Fix double-use of a generation
The first use exhausted it, so the second iteration was empty.
2021-06-14 22:50:22 +02:00
Witcher01
c254b4ad1d fixed password.fetch option not using environment
the '_strategy_command' in 'fetchparams.cli' did not expand the path of
every argument, but only the first one (being the command to be
executed).
i fixed the '_strategy_command' function to apply the 'expand_path'
function to every member of the commands list.
2021-06-13 16:14:46 +02:00
Hugo Osvaldo Barrera
cfd5af832a Fix mixup in the changelog 2021-06-13 16:12:52 +02:00
Hugo Osvaldo Barrera
342cb863cd Merge remote-tracking branch 'dilyanpalauzov/meta_description' 2021-06-13 16:12:09 +02:00
Hugo Osvaldo Barrera
e1c979751d Remove legacy GitHub Action workflows
Conflicts with our newer pipeline when a tag is pushed.
2021-06-13 16:11:13 +02:00
Hugo Osvaldo Barrera
3191886658
Merge pull request #901 from pimutils/publish
Publish tags to PyPI
2021-06-13 15:49:37 +02:00
Hugo Osvaldo Barrera
3260afb495 Publish tags to PyPI 2021-06-13 14:20:28 +02:00
Hugo Osvaldo Barrera
0231f3999e order is probably less important than the others 2021-06-13 00:52:39 +02:00
Hugo Osvaldo Barrera
d1b148c919 Improve docs for new meta properties 2021-06-13 00:49:27 +02:00
Hugo Osvaldo Barrera
e70e8c03e8 Merge branch 'origin/master' into meta_description 2021-06-13 00:46:05 +02:00
Hugo Osvaldo Barrera
46a49e3481
Merge pull request #900 from pimutils/more-git-optimizations
Optimize another very slow test
2021-06-13 00:28:12 +02:00
Hugo Osvaldo Barrera
5e36ca1334 Optimize another very slow test 2021-06-12 23:49:23 +02:00
Hugo Osvaldo Barrera
910317d4bb
Merge pull request #899 from pimutils/faster-tests
Faster tests
2021-06-12 23:41:29 +02:00
Hugo Osvaldo Barrera
29c2b6bb4b git-ignore coverage file 2021-06-12 18:39:28 +02:00
Hugo Osvaldo Barrera
2e4fc7c65a Drop unecessary hypothesis usage
This adds A LOT of time to test execution, for little benefit. While
having random data is good, in reality, most of it was just numbers, and
we rarely got very diverse data.

These hand-picked samples are as good to catch any regressions or issues
with servers.
2021-06-12 18:32:05 +02:00
Hugo Osvaldo Barrera
87f3a594c6
Merge pull request #897 from pimutils/optimise-test-servers
Run test servers as fixtures
2021-06-12 17:13:31 +02:00
Hugo Osvaldo Barrera
d95a8264f4 Merge style dependencies into dev dependencies 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
77d64ddc2c Always check link as part of building docs 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
808e01f9c8 DAVDroid is now called DAVx⁵ 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
86535a9db3 Merge test dependencies into dev dependencies 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
57d662cba1 Drop make target for development packages
We no longer track compat with unreleased packages. Too many variations
and too much complexity.

`vdirsyncer` only works with released packages.
2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
8d62ac4279 Pin the maximum version of click
We're likely won't be compatible with 9.0.
2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
3bf4bd079d Tidy up CI build definitions 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
39ccc168b2 Show a useful help message by default 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
7b0d6671da Delete unused variable 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
8e8c1d5719 Update docs on test DAV servers 2021-06-12 16:28:27 +02:00
Hugo Osvaldo Barrera
299c699cb9 Manage test DAV servers as fixtures
Rather than require manually starting them before tests, manage them as
fixtures an initialise/destroy them as needed.
2021-06-12 16:28:24 +02:00
Hugo Osvaldo Barrera
8cd4a44d02 Simplify definition of DAV_SERVERs 2021-06-12 15:01:08 +02:00
Hugo Osvaldo Barrera
b0f08e051a
Merge pull request #895 from pimutils/optimise-test-servers
Run CI storage tests against Baikal again
2021-06-12 14:21:50 +02:00
Hugo Osvaldo Barrera
4450393d4f Typo
Fixes #892
2021-06-12 13:27:47 +02:00
Hugo Osvaldo Barrera
6c80293a98 Mark failing baikal tests as xfail 2021-06-12 13:26:01 +02:00
Hugo Osvaldo Barrera
a9fa61040f Tidy up some collection initializations 2021-06-12 13:23:54 +02:00
Hugo Osvaldo Barrera
ee124f5c82 Improve readability of failed tests 2021-06-12 13:23:52 +02:00
Hugo Osvaldo Barrera
5a97307a2f Remove unecessary exclusion 2021-06-12 13:08:25 +02:00
Hugo Osvaldo Barrera
e467809bb0 Update pre-commit hooks 2021-06-12 13:08:04 +02:00
Hugo Osvaldo Barrera
320ac6020b Tweak linter to ignore less issues 2021-06-12 12:58:13 +02:00
Hugo Osvaldo Barrera
9d3ef030fa Remove pointless condition
These tasks are named "ci-*", no point in hiding them in other envs.
2021-06-12 12:41:30 +02:00
Hugo Osvaldo Barrera
eec142ac15 Drop mysteryshack support
It's been unmaintained for years, so no point in keeping track of
compatibility.
2021-06-12 12:06:07 +02:00
Hugo Osvaldo Barrera
96f1c41bee Run CI with Baikal too 2021-06-12 12:06:07 +02:00
Hugo Osvaldo Barrera
a0b814ec17 Add a bit of docs to build manifests 2021-06-12 12:06:07 +02:00
Hugo Osvaldo Barrera
91ffb931e1 Move baikal out-of-repo too 2021-06-12 12:06:07 +02:00
Hugo Osvaldo Barrera
bba9d43caf Remove empty install files 2021-06-12 12:06:03 +02:00
Hugo Osvaldo Barrera
ec221b52b4 Update docs with new images names 2021-06-12 12:05:00 +02:00
Hugo Osvaldo Barrera
2336076baf
Merge pull request #894 from pimutils/optimise-test-servers
Move test server dockerfiles out-of-repo
2021-06-12 11:36:15 +02:00
Hugo Osvaldo Barrera
d747977af2 Use externally-built containers for test servers
This speeds up CI by avoiding a rebuild of these container on each run.
2021-06-12 10:59:44 +02:00
Hugo Osvaldo Barrera
0e47775ce3 Remove unused submodules
These submodules were used to bootstrap local test servers for running
integration tests.

We'll be reusing that codebase inside docker containers, but don't want
the containers built for each push to this repo -- that'll happen in a
separate repository.
2021-06-12 10:59:44 +02:00
Hugo Osvaldo Barrera
d6c1b1847c Fix a few linting errors 2021-06-12 10:59:44 +02:00
Bernhard Reiter
95bf683771
Add contrib script to resolve conflicts
Asks the user when two iCalender objects conflict during a sync,
which one to take.
2021-06-11 16:50:00 +02:00
Hugo Osvaldo Barrera
5844480588
Merge pull request #889 from Intevation/dev-issue881
Implement more flexibility for storage/filesystem
2021-06-11 11:37:12 +02:00
Bernhard Reiter
b9f5d88af9
Fixing docs/config.rst and code formatting 2021-06-02 13:52:10 +02:00
Hugo Osvaldo Barrera
5c00cceeb4 Update changelog for 0.18.0 2021-05-31 08:32:18 +02:00
Hugo Barrera
5e3e57ffc2
Merge pull request #891 from pimutils/fix-master
Update click-threading to 0.5.0
2021-05-31 09:56:57 +02:00
Hugo Osvaldo Barrera
addab47786 Update click-threading to 0.5.0
Previous version broke with recent `click` updates.
2021-05-30 22:30:01 +02:00
Hugo Barrera
32bc8d9046
Merge pull request #890 from pimutils/irc
Replace Freenode with Libera.Chat
2021-05-26 17:20:23 +02:00
Hugo Osvaldo Barrera
0d0f2974ae Replace Freenode with Libera.Chat 2021-05-26 00:19:06 +02:00
Bernhard Reiter
9a1582cc0f
Improve storage/filesystem docs
add hints about how to use the `fileext` and `fileignoreext` parameters.
2021-05-21 14:22:30 +02:00
Bernhard Reiter
9b5e01ab38
Improve storage/test_filesystem
with one additional test and fixing of the documentation.
2021-05-20 12:50:05 +02:00
Bernhard Reiter
81895c291e
Make /storage/filesystem more flexible
by adding the optional fileignoreext parameter.
2021-05-19 18:00:09 +02:00
Bernhard Reiter
439e63f8ea
Make /storage/filesystem ignore .tmp files
Hardcode to ignore files with `.tmp` suffix as this is mentioned
in the vdir specification.
2021-05-19 17:51:57 +02:00
Bernhard Reiter
804b9f0429
Add tests to filesystem storage for file ignorance
that ignore .tmp files even when fileext is empty.
Prepares to make the filesystem storage more universal as part of #881 .
2021-05-19 17:40:04 +02:00
Hugo Barrera
44e4beb06f
Merge pull request #886 from pimutils/black
Black
2021-05-11 21:11:09 +02:00
Hugo Barrera
94f8d608ec
Merge pull request #885 from pimutils/deb
Update build script for Debian/Ubuntu
2021-05-11 20:31:42 +02:00
Hugo Osvaldo Barrera
d2d41e5df1 Use black to auto-format the codebase 2021-05-06 19:28:54 +02:00
Hugo Osvaldo Barrera
abf199f21e Fix using mutable object as default arg 2021-05-06 19:28:08 +02:00
Hugo Osvaldo Barrera
75719ecc66 Update pre-commit hooks 2021-05-06 19:26:30 +02:00
Hugo Osvaldo Barrera
9513ee7f61 Update build script for Debian/Ubuntu 2021-05-06 19:11:43 +02:00
Hugo Osvaldo Barrera
a68121e439 Update pre-commit hooks 2021-04-18 16:00:39 +02:00
Hugo Osvaldo Barrera
e355b3336b Fix style checks on CI 2021-04-18 15:59:40 +02:00
Hugo Osvaldo Barrera
b435465bc7 Merge style checks into tests
They're both run in pretty much the same environment, and it's
unnecessary to spin up a separate VM for that.
2021-04-09 20:56:21 +02:00
Hugo Osvaldo Barrera
56688a6c50 Avoid installing any dependencies from PyPI
This is to fully ensure that we're using just distribution packages and
not actually fetching newer stuff from PyPI.
2021-04-09 20:01:27 +02:00
Hugo Barrera
bc002a700e
Merge pull request #879 from pimutils/archlinux
Run tests and CI on ArchLinux image
2021-04-07 21:38:07 +00:00
Hugo Osvaldo Barrera
12c860978b Run CI on ArchLinux with repository dependencies 2021-04-07 21:45:43 +02:00
Hugo Osvaldo Barrera
fdc12d561c Update tests to work with hypothesis 6 2021-04-07 21:45:41 +02:00
Hugo Osvaldo Barrera
f549b1d706 Merge branch 'dav_default_ns' 2021-04-07 09:10:53 +02:00
Hugo Osvaldo Barrera
90e8b7b0bc Merge branch 'cleanup' 2021-04-07 09:10:40 +02:00
Hugo Osvaldo Barrera
c5a59ab10b Delete some obsolete leftovers 2021-04-07 08:43:29 +02:00
Hugo Osvaldo Barrera
2685f8db68 Merge master into dav_default_ns 2021-04-07 08:42:29 +02:00
Hugo Barrera
98d28ea2c0
Merge pull request #876 from dilyanpalauzov/typo_a_an
Typo a→an
2021-04-07 06:41:17 +00:00
Дилян Палаузов
ed0b4bef10 Typo a→an 2021-04-07 09:11:15 +03:00
Hugo Barrera
b9c01f8756
Merge pull request #873 from dilyanpalauzov/typo_google
Typo google→Google
2021-04-06 18:33:34 +00:00
Hugo Barrera
de867fcda2
Merge pull request #851 from pimutils/py39
Add support for Python 3.9
2021-04-06 18:33:08 +00:00
Hugo Osvaldo Barrera
1f066ca6ca Remove unused Makefile target 2021-04-06 19:53:00 +02:00
Hugo Osvaldo Barrera
55af4eaf80 Send coverage reports to Codecov 2021-04-06 19:53:00 +02:00
Hugo Osvaldo Barrera
2161de30d0 Avoid docker-compose
The way it handles docker networks breaks builds.
2021-04-06 19:53:00 +02:00
Hugo Osvaldo Barrera
ef34d77ab1 Use Sourcehut for CI 2021-04-06 19:52:57 +02:00
Hugo Osvaldo Barrera
255ea8f9bf This comparison actually does something 2021-04-05 23:28:39 +02:00
Hugo Osvaldo Barrera
8608f37fbb Pin hypothesis 2021-04-05 23:28:39 +02:00
Hugo Osvaldo Barrera
5ed9c821b8 This should work, right?
It's embarrassing that I didn't come up with something so simple before.
2021-04-05 23:28:39 +02:00
Hugo Osvaldo Barrera
53878f001a Horrible, nasty hack 2021-04-05 23:28:39 +02:00
Hugo Osvaldo Barrera
9df587df26 Update CI image
The former one does not support Python 3.9.
2021-04-05 23:28:39 +02:00
Hugo Osvaldo Barrera
8ac4a00306 Add support for Python 3.9 2021-04-05 23:28:39 +02:00
Дилян Палаузов
7750dda980 Typo google→Google 2021-03-18 16:49:08 +02:00
Дилян Палаузов
bf67af609e Substitute the d: prefix for the DAV: namespace with no prefix (default namespace) 2021-03-09 22:23:12 +02:00
Дилян Палаузов
50cb2def73 Add calendar-description, addressbook-description to the meta data
When a CalDAV collection has calendar-description set and vdirsyncer
synchronizes the “meta” data, synchronize also the calendar description.

-- likewise for addressbook-description and calendar-order
2021-03-08 14:57:48 +02:00
Hugo Barrera
6897995080
Merge pull request #870 from tlahn/patch-1
Replace pipsi with pipx
2021-03-07 19:40:19 +00:00
tlahn
31c60021fa
Replace pipsi with pipx
pipsi page says, it is discontinued. Replacing with pipx
2021-03-07 20:10:39 +01:00
Hugo Barrera
a42906b0e8
Merge pull request #859 from pimutils/tarball-includes
Tweak files included in tarballs
2020-12-28 14:25:46 +00:00
Hugo Osvaldo Barrera
f9c6602684 Exclude docker-related files from tarballs 2020-12-28 10:34:32 +01:00
Hugo Osvaldo Barrera
74bb2ffb66 Include contrib in tarballs
Fixes #858
2020-12-28 10:34:25 +01:00
Hugo Barrera
b5d4d3f9a9
Merge pull request #846 from pfactum/master
contrib/systemd: implement service timeout
2020-10-05 15:57:52 +02:00
Oleksandr Natalenko
f79647b29c contrib/systemd: extend timeout to 3 mins
Some people may have mixes of many servers and/or slow connections,
and we'd rather not break for them.

Signed-off-by: Oleksandr Natalenko <oleksandr@natalenko.name>
2020-10-04 22:08:41 +02:00
Oleksandr Natalenko
1de3632620 contrib/systemd: implement service timeout
Sometimes while doing a suspend/resume cycle vdirsyncer can hang in the
middle of sync, and it never bails out afterwards.

Implement a systemd service change that works around this:

* use `simple` (a default) type for `RuntimeMaxSec` to be effective
* actually set `RuntimeMaxSec` to 30 seconds
* trigger service restart on failure

Signed-off-by: Oleksandr Natalenko <oleksandr@natalenko.name>
2020-10-04 20:14:59 +02:00
Hugo Barrera
aeb46ab5a9
Merge pull request #845 from AlecPapierniak/master
Update installation.rst - add closing quote to virtualenv alias
2020-09-09 08:22:17 +00:00
Alec Papierniak
27ebb0902b
Update installation.rst
adding closing quote to virtualenv alias
2020-09-08 22:29:31 -05:00
Hugo Osvaldo Barrera
f281f956f1 Dedupe import 2020-08-06 12:42:00 +02:00
Hugo Osvaldo Barrera
83e5361643 Fix instructions on how to configure timers
Fixes #792
2020-08-06 12:41:05 +02:00
Hugo Osvaldo Barrera
2650a7ed0b Rework packaging guidelines a bit 2020-08-06 12:39:31 +02:00
Hugo Barrera
68ff37e677
Merge pull request #830 from pimutils/next
Keep moving forward
2020-06-10 19:48:15 +00:00
Hugo Osvaldo Barrera
14deb20ce5 Merge remote-tracking branch 'origin/master' into next 2020-06-10 21:10:02 +02:00
Hugo Osvaldo Barrera
5eef4b1ba1 Document GH Releases and signing 2020-06-10 16:42:35 +02:00
Hugo Osvaldo Barrera
7577fa2117 Update usage of deprecated method 2020-06-09 17:04:46 +02:00
Hugo Osvaldo Barrera
1031b07349 Fix test failures that ONLY happen on master 2020-06-09 17:04:22 +02:00
Hugo Osvaldo Barrera
47caebe843 Simplify coverage handling
- Always install coverage tools.
- Show coverage if all tests pass.
- Less conditional code.
2020-06-09 14:57:58 +02:00
Hugo Osvaldo Barrera
3eb9ce5ae4 Add compatibility with latest click 2020-06-09 14:45:02 +02:00
Hugo Osvaldo Barrera
b1b4dd92fe Sort imports
I don't want to ever have to sort imports again. It's a poor use of
developer time. Automate this with a pre-commit hook, and check this on
CI.

Developers: I suggest you configure your editor to use
`reorder_python_imports`. It uses the standard sorting, and detects
first/third party libs well.
2020-06-09 14:34:45 +02:00
Hugo Osvaldo Barrera
9cb1f8d704 Drop support for Python 3.5 and 3.6 2020-06-09 14:33:14 +02:00
Hugo Osvaldo Barrera
56b1fc2187 Remove now-unused Makefile target 2020-06-09 14:17:56 +02:00
Hugo Osvaldo Barrera
b5dd0929d0 Add GitHub Action to publish releases 2020-06-09 14:10:52 +02:00
Hugo Barrera
d854bd62eb
Merge pull request #825 from pimutils/next
Support Python 3.7 and 3.8
2020-06-09 11:30:34 +00:00
Hugo Osvaldo Barrera
f6e6b0b6c3 Prepare release 0.16.8 2020-06-09 13:27:35 +02:00
Hugo Osvaldo Barrera
399274286e Add classifiers for Python 3.7 and 3.8 2020-06-09 13:01:22 +02:00
Hugo Osvaldo Barrera
be59ba5ab4 Run tests with newer Python versions
We also need different Ubuntu dists for different pythons on travis.
2020-06-09 12:56:10 +02:00
Hugo Osvaldo Barrera
6e59ee0b5f Add a pre-commit hook to rebuild travis config 2020-06-09 12:37:53 +02:00
Hugo Barrera
82375f20aa
Merge pull request #823 from pimutils/next
Run test servers with Docker
2020-06-09 09:47:54 +00:00
Hugo Osvaldo Barrera
cd86ea7a62 Run storage tests for ETESYNC too 2020-06-09 11:29:54 +02:00
Hugo Osvaldo Barrera
354aaec2e0 Only run CI for branch master
For PRs, CI will run on the PR itself, so no need to run it for the
branch (otherwise we get duplicate jobs, and it's sooooo slow!).
2020-06-09 10:46:17 +02:00
Hugo Osvaldo Barrera
af3659ac1a Update contributing instructions 2020-06-09 10:42:26 +02:00
Hugo Osvaldo Barrera
50eefa1816 Use pre-commit for CI style checks
Have a single, uniform definition everywhere.
2020-06-09 10:42:14 +02:00
Hugo Osvaldo Barrera
53331fedee Skip unsupported radicale test 2020-06-09 10:42:14 +02:00
Hugo Osvaldo Barrera
88f2cd5b53 Skip VTODO items for Fastmail
See #824
2020-06-09 10:42:14 +02:00
Hugo Osvaldo Barrera
4f894e04dd Empty noop script 2020-06-09 10:42:14 +02:00
Hugo Osvaldo Barrera
0319035688 Don't run unit/system tests for each storage
The current storage (DAV_SERVER) has no impact on unit tests and system
tests, so rather than run all these tests for each server, just run them
once per python.
2020-06-09 10:42:11 +02:00
Hugo Osvaldo Barrera
6c6da2f613 Test baikal using docker 2020-06-09 09:49:18 +02:00
Hugo Osvaldo Barrera
b0d8fd34dc Test radicale with docker too 2020-06-09 09:49:16 +02:00
Hugo Osvaldo Barrera
0f3b2e74c0 Test xandikos running it in docker
The main advantage here is that its dependencies are TOTALLY separate
from vdirsyncer's, keeping the runtime environment for vdirsyncer
cleaner.

It also makes testing locally not only possible, but fast and pleasant.
2020-06-09 09:49:03 +02:00
Hugo Osvaldo Barrera
c410fbf331 Enable docker on travis 2020-06-09 09:46:27 +02:00
Hugo Osvaldo Barrera
f1f51ac3cf Don't fix EOF in travis.yml
Since this is auto-generated, it... conflicts in complicated ways. 😞
2020-06-09 09:46:27 +02:00
Hugo Osvaldo Barrera
3037c15a65 Use hypothesis setting load_profile to setup health check
Fixes #779
2020-06-08 19:58:35 +02:00
Romain
e5caf6750d Add double quote in exemple config files (#732)
* nextcloud.rst : add double quote to not forget them

Add double quote to not forget them, and avoid the message :
warning: Soon, all strings have to be in double quotes. Please replace UserName with "UserName"

* fastmail.rst : add double quote to not forget them

Add double quote to not forget them, and avoid the message :
warning: Soon, all strings have to be in double quotes. Please replace UserName with "UserName"

* icloud.rst : add double quote to not forget them

Add double quote to not forget them, and avoid the message :
warning: Soon, all strings have to be in double quotes. Please replace UserName with "UserName"

* todoman.rst : add double quote to not forget them

Add double quote to not forget them, and avoid the message :
warning: Soon, all strings have to be in double quotes. Please replace UserName with "UserName"

* xandikos.rst : add double quote to not forget them

Add double quote to not forget them, and avoid the message :
warning: Soon, all strings have to be in double quotes. Please replace UserName with "UserName"

* davmail.rst : add double quote to not forget them

Add double quote to not forget them, and avoid the message :
warning: Soon, all strings have to be in double quotes. Please replace UserName with "UserName"

* partial-sync.rst : add double quote to not forget them

Add double quote to not forget them, and avoid the message :
warning: Soon, all strings have to be in double quotes. Please replace UserName with "UserName"
2020-06-08 19:58:35 +02:00
Markus Unterwaditzer
f0fe104427 Credit packagecloud
Because we asked packagecloud for more bandwidth, they asked us to
credit them in the README
2020-06-08 19:58:35 +02:00
Hugo Osvaldo Barrera
5c3900500d Update link to official Arch package (#710)
There's now an official Arch package
2020-06-08 19:58:35 +02:00
Markus Unterwaditzer
6befffcc45 Screw git hooks 2020-06-08 19:58:35 +02:00
Markus Unterwaditzer
22717ee217 Add simple doc for todoman 2020-06-08 19:58:35 +02:00
Amanda Hickman
c78ec6b3bd Little spelling fix (#695)
* Fixed spelling of "occurred"

* Fix spelling of occurred.

* fixed one lingering misspelling
2020-06-08 19:58:35 +02:00
Hugo Osvaldo Barrera
289f60da44 Update copyright year 2020-06-08 19:58:35 +02:00
Malte Kiefer
69e235c35d fixed typo (#678)
fixed typo
2020-06-08 19:09:59 +02:00
Markus Unterwaditzer
5a2032d6d9 Add fast_finish to Travis 2020-06-08 19:09:59 +02:00
Markus Unterwaditzer
fb68a6c4aa Fix broken link 2020-06-08 19:09:59 +02:00
Markus Unterwaditzer
107edfd52d Fix installation link 2020-06-08 19:09:59 +02:00
Hugo Osvaldo Barrera
60e2e9669e Merge branch 'next' 2020-06-08 18:59:37 +02:00
Thomas Klausner
b1214cd693 Follow advice from setuptools_scm how to use it.
See https://github.com/pypa/setuptools_scm
2020-06-08 18:58:27 +02:00
/\/\ \-/ ❭❬
3d7d92c2d6 baikal-server.com does not work anymore 2020-06-08 18:56:26 +02:00
5472qaywsx
59740b379f Update claws-mail.rst
Fixed some (2) minor typos.
Actually, all I wanted to do is to fix typo "contab -e" -> "crontab -e" of page "https://vdirsyncer.pimutils.org/en/stable/tutorials/claws-mail.html", but seems already correct on github...

Closes #803
2020-06-08 18:53:12 +02:00
Hugo Osvaldo Barrera
461e4c55b0 Update servers for Fastmail
Fixes #785
2020-06-08 18:50:03 +02:00
Hugo Osvaldo Barrera
a5b98517e8 Remove broken waffle badge 2020-06-08 18:45:04 +02:00
Hugo Osvaldo Barrera
1e425a590a Run CI on this branch too 2020-06-08 18:41:55 +02:00
Hugo Osvaldo Barrera
308289febf Remove python2 leftover code 2020-06-08 18:40:33 +02:00
Hugo Osvaldo Barrera
eece9a6bde Update requests
2.20.0 is almost two years old, so we can safely assume anyone wanting a
recent vdirsyncer can upgrade.

It's also the first version to include several upstream security fixes
which I'd rather we depend on too.
2020-06-08 18:40:33 +02:00
Hugo Osvaldo Barrera
a26d3bb58c Update hypothesis 2020-06-08 18:40:33 +02:00
Hugo Osvaldo Barrera
e2d3c1add7 Add pre-commit hooks with some basic linting
Mostly to keep an eye on code quality as I move forward.
2020-06-08 18:40:33 +02:00
Hugo Osvaldo Barrera
b4bbc5946a Simplify travis setup a bit 2020-06-08 18:40:33 +02:00
Hugo Osvaldo Barrera
7e4a0be674 Upgrade flake8
Update to the latest flake8, and fix all code warnings related to that.
2020-06-08 18:40:31 +02:00
Hugo Osvaldo Barrera
aafafaa501 Pin dependencies for ete tests 2020-06-08 17:16:28 +02:00
Hugo Osvaldo Barrera
9505430b83 Pin an older version of Xandikos
This is the one used back when master was last green, so let's start
with this one and gradually move up.
2020-06-08 16:33:17 +02:00
Hugo Osvaldo Barrera
216ce8d180 Disable CI on OSX for now
Just gets in the way, and no change of having that green until we update
a lot of other things.
2020-06-08 16:33:12 +02:00
Hugo Osvaldo Barrera
cb4ba5b38c Fix linting errors
One more green checkmark! 
2020-06-08 15:39:01 +02:00
Hugo Osvaldo Barrera
72ea0a6ad3 Pin etesync 2020-06-08 15:31:20 +02:00
Hugo Osvaldo Barrera
810349eef0 Pin an older flake8 until we get around to linting 2020-06-08 15:00:38 +02:00
Hugo Osvaldo Barrera
65d17bdcbf Pin a known-working version of radicale too 2020-06-08 14:47:41 +02:00
Hugo Osvaldo Barrera
470c2c6630 Don't test with devel versions
There's no chance this is going to work -- we've a lot of catching up to
do in this aspect.
2020-06-08 14:44:55 +02:00
Hugo Osvaldo Barrera
7c04289ed4 Pin xandikos to 0.1.0 2020-06-08 13:51:15 +02:00
Hugo Osvaldo Barrera
e987d6eb4a Drop Python 3.4
Never gonna get master back to green working with a dead Python.
2020-06-08 13:34:17 +02:00
Hugo Osvaldo Barrera
558da29e5e Use the syntax hypothesis accepts 2020-06-08 13:26:39 +02:00
Hugo Osvaldo Barrera
83fe7d2c8a Pin dependencies to older versions
Just to get master back up and running again, pin libraries to the
versions we used back when the latest development happened.

I'll start upgrading them gradually, but need a green master first.
2020-06-08 13:15:27 +02:00
Martin Michlmayr
78599a131d Fix typos 2020-06-08 13:02:36 +02:00
Markus Unterwaditzer
dcf5f701b7 Version 0.16.7 2018-07-19 21:15:29 +02:00
Markus Unterwaditzer
80a42e4c6c Fixes for open_graphical_browser, fixes #754 2018-07-19 21:14:30 +02:00
158 changed files with 7260 additions and 9916 deletions

View file

@ -0,0 +1,49 @@
# Run tests using the packaged dependencies on ArchLinux.
image: archlinux
packages:
- docker
- docker-compose
# Build dependencies:
- python-wheel
- python-build
- python-installer
- python-setuptools-scm
# Runtime dependencies:
- python-click
- python-click-log
- python-click-threading
- python-requests
- python-aiohttp-oauthlib
- python-tenacity
# Test dependencies:
- python-hypothesis
- python-pytest-cov
- python-pytest-httpserver
- python-trustme
- python-pytest-asyncio
- python-aiohttp
- python-aiostream
- python-aioresponses
sources:
- https://github.com/pimutils/vdirsyncer
environment:
BUILD: test
CI: true
CODECOV_TOKEN: b834a3c5-28fa-4808-9bdb-182210069c79
DAV_SERVER: radicale xandikos
REQUIREMENTS: release
# TODO: ETESYNC_TESTS
tasks:
- check-python:
python --version | grep 'Python 3.13'
- docker: |
sudo systemctl start docker
- setup: |
cd vdirsyncer
python -m build --wheel --skip-dependency-check --no-isolation
sudo python -m installer dist/*.whl
- test: |
cd vdirsyncer
make -e ci-test
make -e ci-test-storage

36
.builds/tests-minimal.yml Normal file
View file

@ -0,0 +1,36 @@
# Run tests using oldest available dependency versions.
#
# TODO: It might make more sense to test with an older Ubuntu or Fedora version
# here, and consider that our "oldest suppported environment".
image: alpine/3.19 # python 3.11
packages:
- docker
- docker-cli
- docker-compose
- py3-pip
- python3-dev
sources:
- https://github.com/pimutils/vdirsyncer
environment:
BUILD: test
CI: true
CODECOV_TOKEN: b834a3c5-28fa-4808-9bdb-182210069c79
DAV_SERVER: radicale xandikos
REQUIREMENTS: minimal
tasks:
- venv: |
python3 -m venv $HOME/venv
echo "export PATH=$HOME/venv/bin:$PATH" >> $HOME/.buildenv
- docker: |
sudo addgroup $(whoami) docker
sudo service docker start
- setup: |
cd vdirsyncer
# Hack, no idea why it's needed
sudo ln -s /usr/include/python3.11/cpython/longintrepr.h /usr/include/python3.11/longintrepr.h
make -e install-dev
- test: |
cd vdirsyncer
make -e ci-test
make -e ci-test-storage

45
.builds/tests-pypi.yml Normal file
View file

@ -0,0 +1,45 @@
# Run tests using latest dependencies from PyPI
image: archlinux
packages:
- docker
- docker-compose
- python-pip
sources:
- https://github.com/pimutils/vdirsyncer
secrets:
- 4d9a6dfe-5c8d-48bd-b864-a2f5d772c536
environment:
BUILD: test
CI: true
CODECOV_TOKEN: b834a3c5-28fa-4808-9bdb-182210069c79
DAV_SERVER: baikal radicale xandikos
REQUIREMENTS: release
# TODO: ETESYNC_TESTS
tasks:
- venv: |
python -m venv $HOME/venv
echo "export PATH=$HOME/venv/bin:$PATH" >> $HOME/.buildenv
- docker: |
sudo systemctl start docker
- setup: |
cd vdirsyncer
make -e install-dev
- test: |
cd vdirsyncer
make -e ci-test
make -e ci-test-storage
- check: |
cd vdirsyncer
make check
- check-secrets: |
# Stop here if this is a PR. PRs can't run with the below secrets.
[ -f ~/fastmail-secrets ] || complete-build
- extra-storages: |
set +x
source ~/fastmail-secrets
set -x
cd vdirsyncer
export PATH=$PATH:~/.local/bin/
DAV_SERVER=fastmail pytest tests/storage

View file

@ -1,243 +0,0 @@
version: 2
references:
basic_env: &basic_env
CI: true
restore_caches: &restore_caches
restore_cache:
keys:
- cache3-{{ arch }}-{{ .Branch }}
save_caches: &save_caches
save_cache:
key: cache3-{{ arch }}-{{ .Branch }}
paths:
- "rust/target/"
- "~/.cargo/"
- "~/.cache/pip/"
- "~/.rustup/"
basic_setup: &basic_setup
run: . scripts/circleci-install.sh
jobs:
nextcloud:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
NEXTCLOUD_HOST: localhost:80
DAV_SERVER: nextcloud
- image: nextcloud
environment:
SQLITE_DATABASE: nextcloud
NEXTCLOUD_ADMIN_USER: asdf
NEXTCLOUD_ADMIN_PASSWORD: asdf
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: wget -O - --retry-connrefused http://localhost:80/
- run: make -e storage-test
fastmail:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
DAV_SERVER: fastmail
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e storage-test
icloud:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
DAV_SERVER: icloud
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e storage-test
davical:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
DAV_SERVER: davical
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e storage-test
xandikos:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
DAV_SERVER: xandikos
- image: vdirsyncer/xandikos:0.0.1
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: wget -O - --retry-connrefused http://localhost:5001/
- run: make -e storage-test
style:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-style
- *save_caches
- run: make -e style
py34-minimal:
docker:
- image: circleci/python:3.4
environment:
<<: *basic_env
REQUIREMENTS: minimal
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e test
py34-release:
docker:
- image: circleci/python:3.4
environment:
<<: *basic_env
REQUIREMENTS: release
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e test
py34-devel:
docker:
- image: circleci/python:3.4
environment:
<<: *basic_env
REQUIREMENTS: devel
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e test
py36-minimal:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
REQUIREMENTS: minimal
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e test
py36-release:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
REQUIREMENTS: release
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e test
py36-devel:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
REQUIREMENTS: devel
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e test
rust:
docker:
- image: circleci/python:3.6
environment:
<<: *basic_env
REQUIREMENTS: release
steps:
- checkout
- *restore_caches
- *basic_setup
- run: make -e install-dev install-test
- *save_caches
- run: make -e rust-test
workflows:
version: 2
test_all:
jobs:
- nextcloud
- fastmail
- icloud
- davical
- xandikos
- style
- py34-minimal
- py34-release
- py34-devel
- py36-minimal
- py36-release
- py36-devel
- rust

View file

@ -2,10 +2,3 @@ comment: false
coverage:
status:
patch: false
project:
unit:
flags: unit
system:
flags: system
storage:
flags: storage

1
.envrc Normal file
View file

@ -0,0 +1 @@
layout python3

3
.gitignore vendored
View file

@ -13,6 +13,5 @@ env
dist
docs/_build/
vdirsyncer/version.py
vdirsyncer/_native*
.hypothesis
codecov.sh
coverage.xml

0
.gitmodules vendored
View file

39
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,39 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-toml
- id: check-added-large-files
- id: debug-statements
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.15.0"
hooks:
- id: mypy
files: vdirsyncer/.*
additional_dependencies:
- types-setuptools
- types-docutils
- types-requests
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.11.4'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: local
hooks:
- id: typos-syncroniz
name: typos-syncroniz
language: system
# Not how you spell "synchronise"
entry: sh -c "git grep -i syncroniz"
files: ".*/.*"
- id: typos-text-icalendar
name: typos-text-icalendar
language: system
# It's "text/calendar", no "i".
entry: sh -c "git grep -i 'text/icalendar'"
files: ".*/.*"

16
.readthedocs.yaml Normal file
View file

@ -0,0 +1,16 @@
version: 2
sphinx:
configuration: docs/conf.py
build:
os: "ubuntu-22.04"
tools:
python: "3.9"
python:
install:
- method: pip
path: .
extra_requirements:
- docs

View file

@ -4,15 +4,22 @@ Contributors
In alphabetical order:
- Ben Boeckel
- Bleala
- Christian Geier
- Clément Mondon
- Corey Hinshaw
- Kai Herlemann
- Hugo Osvaldo Barrera
- Jason Cox
- Julian Mehne
- Malte Kiefer
- Marek Marczykowski-Górecki
- Markus Unterwaditzer
- Michael Adler
- rEnr3n
- Thomas Weißschuh
- Witcher01
- samm81
Special thanks goes to:

View file

@ -9,12 +9,142 @@ Package maintainers and users who have to manually update their installation
may want to subscribe to `GitHub's tag feed
<https://github.com/pimutils/vdirsyncer/tags.atom>`_.
Version 0.17.0
Version 0.21.0
==============
- Fix bug where collection discovery under DAV-storages would produce invalid
XML. See :gh:`688`.
- ownCloud and Baikal are no longer tested.
- Implement retrying for ``google`` storage type when a rate limit is reached.
- ``tenacity`` is now a required dependency.
- Drop support for Python 3.8.
- Retry transient network errors for nullipotent requests.
Version 0.20.0
==============
- Remove dependency on abandoned ``atomicwrites`` library.
- Implement ``filter_hook`` for the HTTP storage.
- Drop support for Python 3.7.
- Add support for Python 3.12 and Python 3.13.
- Properly close the status database after using. This especially affects tests,
where we were leaking a large amount of file descriptors.
- Extend supported versions of ``aiostream`` to include 0.7.x.
Version 0.19.3
==============
- Added a no_delete option to the storage configuration. :gh:`1090`
- Fix crash when running ``vdirsyncer repair`` on a collection. :gh:`1019`
- Add an option to request vCard v4.0. :gh:`1066`
- Require matching ``BEGIN`` and ``END`` lines in vobjects. :gh:`1103`
- A Docker environment for Vdirsyncer has been added `Vdirsyncer DOCKERIZED <https://github.com/Bleala/Vdirsyncer-DOCKERIZED>`_.
- Implement digest auth. :gh:`1137`
- Add ``filter_hook`` parameter to :storage:`http`. :gh:`1136`
Version 0.19.2
==============
- Improve the performance of ``SingleFileStorage``. :gh:`818`
- Properly document some caveats of the Google Contacts storage.
- Fix crash when using auth certs. :gh:`1033`
- The ``filesystem`` storage can be specified with ``type =
"filesystem/icalendar"`` or ``type = "filesystem/vcard"``. This has not
functional impact, and is merely for forward compatibility with the Rust
implementation of vdirsyncer.
- Python 3.10 and 3.11 are officially supported.
- Instructions for integrating with Google CalDav/CardDav have changed.
Applications now need to be registered as "Desktop applications". Using "Web
application" no longer works due to changes on Google's side. :gh:`1078`
Version 0.19.1
==============
- Fixed crash when operating on Google Contacts. :gh:`994`
- The ``HTTP_PROXY`` and ``HTTPS_PROXY`` are now respected. :gh:`1031`
- Instructions for integrating with Google CalDav/CardDav have changed.
Applications now need to be registered as "Web Application". :gh:`975`
- Various documentation updates.
Version 0.19.0
==============
- Add "shell" password fetch strategy to pass command string to a shell.
- Add "description" and "order" as metadata. These fetch the CalDAV:
calendar-description, ``CardDAV:addressbook-description`` and
``apple-ns:calendar-order`` properties respectively.
- 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
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`
- ``pytest-httpserver`` and ``trustme`` are now required for tests.
- ``pytest-localserver`` is no longer required for tests.
- Multithreaded support has been dropped. The ``"--max-workers`` has been removed.
- A new ``asyncio`` backend is now used. So far, this shows substantial speed
improvements in ``discovery`` and ``metasync``, but little change in `sync`.
This will likely continue improving over time. :gh:`906`
- The ``google`` storage types no longer require ``requests-oauthlib``, but
require ``python-aiohttp-oauthlib`` instead.
- Vdirsyncer no longer includes experimental support for `EteSync
<https://www.etesync.com/>`_. The existing integration had not been supported
for a long time and no longer worked. Support for external storages may be
added if anyone is interested in maintaining an EteSync plugin. EteSync
users should consider using `etesync-dav`_.
- The ``plist`` for macOS has been dropped. It was broken and homebrew
generates their own based on package metadata. macOS users are encouraged to
use that as a reference.
.. _etesync-dav: https://github.com/etesync/etesync-dav
Changes to SSL configuration
----------------------------
Support for ``md5`` and ``sha1`` certificate fingerprints has been dropped. If
you're validating certificate fingerprints, use ``sha256`` instead.
When using a custom ``verify_fingerprint``, CA validation is always disabled.
If ``verify_fingerprint`` is unset, CA verification is always active. Disabling
both features is insecure and no longer supported.
The ``verify`` parameter no longer takes boolean values, it is now optional and
only takes a string to a custom CA for verification.
The ``verify`` and ``verify_fingerprint`` will likely be merged into a single
parameter in future.
Version 0.18.0
==============
Note: Version 0.17 has some alpha releases but ultimately was never finalised.
0.18 actually continues where 0.16 left off.
- Support for Python 3.5 and 3.6 has been dropped. This release mostly focuses
on keeping vdirsyncer compatible with newer environments.
- click 8 and click-threading 0.5.0 are now required.
- For those using ``pipsi``, we now recommend using ``pipx``, it's successor.
- Python 3.9 is now supported.
- Our Debian/Ubuntu build scripts have been updated. New versions should be
pushed to those repositories soon.
Version 0.16.8
==============
*released 09 June 2020*
- Support Python 3.7 and 3.8.
This release is functionally identical to 0.16.7.
It's been tested with recent Python versions, and has been marked as supporting
them. It will also be the final release supporting Python 3.5 and 3.6.
Version 0.16.7
==============
*released on 19 July 2018*
- Fixes for Python 3.7
Version 0.16.6
==============
@ -120,7 +250,7 @@ Version 0.14.0
exit code in such situations is still non-zero.
- Add ``partial_sync`` option to pair section. See :ref:`the config docs
<partial_sync_def>`.
- Vdirsyner will now warn if there's a string without quotes in your config.
- Vdirsyncer will now warn if there's a string without quotes in your config.
Please file issues if you find documentation that uses unquoted strings.
- Fix an issue that would break khal's config setup wizard.

View file

@ -1,4 +1,4 @@
Copyright (c) 2014-2018 by Markus Unterwaditzer & contributors. See
Copyright (c) 2014-2020 by Markus Unterwaditzer & contributors. See
AUTHORS.rst for more details.
Some rights reserved.
@ -31,10 +31,3 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
== etesync ==
I, Tom Hacohen, hereby grant a license for EteSync's journal-manager
(https://github.com/etesync/journal-manager) to be used as a dependency in
vdirsyncer's test suite for the purpose of testing vdirsyncer without having
the copyleft section of the AGPL apply to it (vdirsyncer).

View file

@ -1,8 +1,7 @@
# setuptools-scm includes everything tracked by git
prune contrib
prune docker
prune scripts
prune tests/storage/servers
prune tests/storage/etesync
recursive-include tests/storage/servers/radicale *
recursive-include tests/storage/servers/skip *

142
Makefile
View file

@ -1,7 +1,7 @@
# See the documentation on how to run the tests:
# https://vdirsyncer.pimutils.org/en/stable/contributing.html
# Which DAV server to run the tests against (radicale, xandikos, skip, nextcloud, ...)
# Which DAV server to run the tests against (radicale, xandikos, skip, owncloud, nextcloud, ...)
export DAV_SERVER := skip
# release (install release versions of dependencies)
@ -12,152 +12,52 @@ export REQUIREMENTS := release
# Set this to true if you run vdirsyncer's test as part of e.g. packaging.
export DETERMINISTIC_TESTS := false
# Run the etesync testsuite.
export ETESYNC_TESTS := false
# Assume to run in Travis. Don't use this outside of a virtual machine. It will
# Assume to run in CI. Don't use this outside of a virtual machine. It will
# heavily "pollute" your system, such as attempting to install a new Python
# systemwide.
export CI := false
# Enable debug symbols and backtrace printing for rust lib
export RUST_BACKTRACE := $(CI)
# Whether to generate coverage data while running tests.
export COVERAGE := $(CI)
# Log everything
export RUST_LOG := vdirsyncer_rustext=debug
# Additional arguments that should be passed to py.test.
PYTEST_ARGS =
# Variables below this line are not very interesting for getting started.
TEST_EXTRA_PACKAGES =
ifeq ($(COVERAGE), true)
TEST_EXTRA_PACKAGES += pytest-cov
PYTEST_ARGS += --cov-config .coveragerc --cov vdirsyncer
endif
ifeq ($(ETESYNC_TESTS), true)
TEST_EXTRA_PACKAGES += django-etesync-journal django djangorestframework wsgi_intercept drf-nested-routers
endif
PYTEST = py.test $(PYTEST_ARGS)
export TESTSERVER_BASE := ./tests/storage/servers/
CODECOV_PATH = /tmp/codecov.sh
all:
$(error Take a look at https://vdirsyncer.pimutils.org/en/stable/tutorial.html#installation)
ifeq ($(CI), true)
codecov.sh:
curl -s https://codecov.io/bash > $@
else
codecov.sh:
echo > $@
endif
ci-test:
curl -s https://codecov.io/bash > $(CODECOV_PATH)
pytest --cov vdirsyncer --cov-append tests/unit/ tests/system/
bash $(CODECOV_PATH) -c
rust-test:
cd rust/ && cargo test --release
test: unit-test system-test storage-test
unit-test: codecov.sh
$(PYTEST) tests/unit/
bash codecov.sh -c -F unit
system-test: codecov.sh
$(PYTEST) tests/system/
bash codecov.sh -c -F system
storage-test: codecov.sh
$(PYTEST) tests/storage/
bash codecov.sh -c -F storage
install-servers:
ci-test-storage:
curl -s https://codecov.io/bash > $(CODECOV_PATH)
set -ex; \
for server in $(DAV_SERVER); do \
if [ ! "$$(ls $(TESTSERVER_BASE)$$server/)" ]; then \
git submodule update --init -- "$(TESTSERVER_BASE)$$server"; \
fi; \
(cd $(TESTSERVER_BASE)$$server && sh install.sh); \
DAV_SERVER=$$server pytest --cov vdirsyncer --cov-append tests/storage; \
done
bash $(CODECOV_PATH) -c
install-test: install-servers
pip install -Ur test-requirements.txt
set -xe && if [ "$$REQUIREMENTS" = "devel" ]; then \
pip install -U --force-reinstall \
'git+https://github.com/HypothesisWorks/hypothesis#egg=hypothesis&subdirectory=hypothesis-python' \
git+https://github.com/kennethreitz/requests \
git+https://github.com/pytest-dev/pytest; \
fi
[ -z "$(TEST_EXTRA_PACKAGES)" ] || pip install $(TEST_EXTRA_PACKAGES)
install-style: install-docs
pip install -U flake8 flake8-import-order 'flake8-bugbear>=17.3.0'
rustup component add rustfmt-preview
cargo install --force --git https://github.com/rust-lang-nursery/rust-clippy clippy
style:
flake8
! git grep -i syncroniz */*
! git grep -i 'text/icalendar' */*
sphinx-build -W -b html ./docs/ ./docs/_build/html/
cd rust/ && cargo +nightly clippy
cd rust/ && cargo +nightly fmt --all -- --check
install-docs:
pip install -Ur docs-requirements.txt
docs:
cd docs && make html
linkcheck:
sphinx-build -W -b linkcheck ./docs/ ./docs/_build/linkcheck/
release:
python setup.py sdist upload
check:
ruff check
ruff format --diff
#mypy vdirsyncer
release-deb:
sh scripts/release-deb.sh debian jessie
sh scripts/release-deb.sh debian stretch
sh scripts/release-deb.sh ubuntu trusty
sh scripts/release-deb.sh ubuntu xenial
sh scripts/release-deb.sh ubuntu zesty
install-dev:
pip install -ve .
[ "$(ETESYNC_TESTS)" = "false" ] || pip install -Ue .[etesync]
set -xe && if [ "$(REQUIREMENTS)" = "devel" ]; then \
pip install -U --force-reinstall \
git+https://github.com/mitsuhiko/click \
git+https://github.com/click-contrib/click-log \
git+https://github.com/kennethreitz/requests; \
elif [ "$(REQUIREMENTS)" = "minimal" ]; then \
pip install -U --force-reinstall $$(python setup.py --quiet minimal_requirements); \
pip install -U pip setuptools wheel
pip install -e '.[test,check,docs]'
set -xe && if [ "$(REQUIREMENTS)" = "minimal" ]; then \
pip install pyproject-dependencies && \
pip install -U --force-reinstall $$(pyproject-dependencies . | sed 's/>/=/'); \
fi
ssh-submodule-urls:
git submodule foreach "\
echo -n 'Old: '; \
git remote get-url origin; \
git remote set-url origin \$$(git remote get-url origin | sed -e 's/https:\/\/github\.com\//git@github.com:/g'); \
echo -n 'New URL: '; \
git remote get-url origin"
install-rust:
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly
rustup update nightly
rust/vdirsyncer_rustext.h:
cd rust/ && cargo build # hack to work around cbindgen bugs
CARGO_EXPAND_TARGET_DIR=rust/target/ cbindgen -c rust/cbindgen.toml rust/ > $@
docker/xandikos:
docker build -t vdirsyncer/xandikos:0.0.1 $@
docker push vdirsyncer/xandikos:0.0.1
.PHONY: docs rust/vdirsyncer_rustext.h docker/xandikos
.PHONY: docs

View file

@ -2,6 +2,30 @@
vdirsyncer
==========
.. image:: https://builds.sr.ht/~whynothugo/vdirsyncer.svg
:target: https://builds.sr.ht/~whynothugo/vdirsyncer
:alt: CI status
.. image:: https://codecov.io/github/pimutils/vdirsyncer/coverage.svg?branch=main
:target: https://codecov.io/github/pimutils/vdirsyncer?branch=main
:alt: Codecov coverage report
.. image:: https://readthedocs.org/projects/vdirsyncer/badge/
:target: https://vdirsyncer.rtfd.org/
:alt: documentation
.. image:: https://img.shields.io/pypi/v/vdirsyncer.svg
:target: https://pypi.python.org/pypi/vdirsyncer
:alt: version on pypi
.. image:: https://img.shields.io/badge/deb-packagecloud.io-844fec.svg
:target: https://packagecloud.io/pimutils/vdirsyncer
:alt: Debian packages
.. image:: https://img.shields.io/pypi/l/vdirsyncer.svg
:target: https://github.com/pimutils/vdirsyncer/blob/main/LICENCE
:alt: licence: BSD
- `Documentation <https://vdirsyncer.pimutils.org/en/stable/>`_
- `Source code <https://github.com/pimutils/vdirsyncer>`_
@ -16,22 +40,10 @@ servers. It can also be used to synchronize calendars and/or addressbooks
between two servers directly.
It aims to be for calendars and contacts what `OfflineIMAP
<http://offlineimap.org/>`_ is for emails.
<https://www.offlineimap.org/>`_ is for emails.
.. _programs: https://vdirsyncer.pimutils.org/en/latest/tutorials/
.. image:: https://circleci.com/gh/pimutils/vdirsyncer.svg?style=shield
:target: https://circleci.com/gh/pimutils/vdirsyncer
.. image:: https://codecov.io/github/pimutils/vdirsyncer/coverage.svg?branch=master
:target: https://codecov.io/github/pimutils/vdirsyncer?branch=master
.. image:: https://badge.waffle.io/pimutils/vdirsyncer.svg?label=ready&title=Ready
:target: https://waffle.io/pimutils/vdirsyncer
.. image:: https://img.shields.io/badge/deb-packagecloud.io-844fec.svg
:target: https://packagecloud.io/pimutils/vdirsyncer
Links of interest
=================
@ -47,6 +59,15 @@ Links of interest
* `Donations <https://vdirsyncer.pimutils.org/en/stable/donations.html>`_
Dockerized
=================
If you want to run `Vdirsyncer <https://vdirsyncer.pimutils.org/en/stable/>`_ in a
Docker environment, you can check out the following GitHub Repository:
* `Vdirsyncer DOCKERIZED <https://github.com/Bleala/Vdirsyncer-DOCKERIZED>`_
Note: This is an unofficial Docker build, it is maintained by `Bleala <https://github.com/Bleala>`_.
License
=======

View file

@ -43,7 +43,7 @@ fileext = ".vcf"
[storage bob_contacts_remote]
type = "carddav"
url = "https://nextcloud.example.com/"
url = "https://owncloud.example.com/remote.php/carddav/"
#username =
# The password can also be fetched from the system password storage, netrc or a
# custom command. See http://vdirsyncer.pimutils.org/en/stable/keyring.html
@ -65,6 +65,6 @@ fileext = ".ics"
[storage bob_calendar_remote]
type = "caldav"
url = "https://nextcloud.example.com/"
url = "https://owncloud.example.com/remote.php/caldav/"
#username =
#password =

View file

@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Ask user to resolve a vdirsyncer sync conflict interactively.
Needs a way to ask the user.
The use of https://apps.kde.org/kdialog/ for GNU/Linix is hardcoded.
Depends on python>3.5 and KDialog.
Usage:
Ensure the file executable and use it in the vdirsyncer.conf file, e.g.
conflict_resolution = ["command", "/home/bern/vdirsyncer/resolve_interactively.py"]
This file is Free Software under the following license:
SPDX-License-Identifier: BSD-3-Clause
SPDX-FileCopyrightText: 2021 Intevation GmbH <https://intevation.de>
Author: <bernhard.reiter@intevation.de>
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
KDIALOG = "/usr/bin/kdialog"
SUMMARY_PATTERN = re.compile("^(SUMMARY:.*)$", re.MULTILINE)
def get_summary(icalendar_text: str):
"""Get the first SUMMARY: line from an iCalendar text.
Do not care about the line being continued.
"""
match = re.search(SUMMARY_PATTERN, icalendar_text)
return match[1]
def main(ical1_filename, ical2_filename):
ical1 = ical1_filename.read_text()
ical2 = ical2_filename.read_text()
additional_args = ["--yes-label", "take first"] # return code == 0
additional_args += ["--no-label", "take second"] # return code == 1
additional_args += ["--cancel-label", "do not resolve"] # return code == 2
r = subprocess.run(
args=[
KDIALOG,
"--warningyesnocancel",
"There was a sync conflict, do you prefer the first entry: \n"
f"{get_summary(ical1)}...\n(full contents: {ical1_filename})\n\n"
"or the second entry:\n"
f"{get_summary(ical2)}...\n(full contents: {ical2_filename})?",
*additional_args,
]
)
if r.returncode == 2:
# cancel was pressed
return # shall lead to items not changed, because not copied
if r.returncode == 0:
# we want to take the first item, so overwrite the second
ical2_filename.write_text(ical1)
else: # r.returncode == 1, we want the second item, so overwrite the first
ical1_filename.write_text(ical2)
if len(sys.argv) != 3:
sys.stdout.write(__doc__)
else:
main(Path(sys.argv[1]), Path(sys.argv[2]))

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- Blueprint for cron-like launchd plist -->
<!-- Replace @@PLACEHOLDERS@@ with appropriate values for your system/settings! -->
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<!-- Locale to use for vdirsyncer, e.g. en_US.UTF-8 -->
<key>LANG</key>
<string>@@LOCALE@@</string>
<key>LC_ALL</key>
<string>@@LOCALE@@</string>
</dict>
<key>Label</key>
<string>vdirsyncer</string>
<key>WorkingDirectory</key>
<!-- working directory for vdirsyncer, usually the base directory where
vdirsyncer is installed, e.g. /usr/local/ -->
<string>@@WORKINGDIRECTORY@@</string>
<key>ProgramArguments</key>
<array>
<!-- full path to vdirsyncer binary -->
<string>@@VDIRSYNCER@@</string>
<!-- only log errors -->
<string>-v</string>
<string>ERROR</string>
<string>sync</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<!-- Sync intervall in seconds -->
<integer>@@SYNCINTERVALL@@</integer>
<!-- For logging, redirect stdout & stderr -->
<!-- <key>StandardErrorPath</key> -->
<!-- Full path to stderr logfile, e.g. /tmp/vdirsyncer_err.log -->
<!-- <string>@@STDERRFILE@@</string> -->
<!-- Full path to stdout logfile, e.g. /tmp/vdirsyncer_out.log -->
<!-- <key>StandardOutPath</key> -->
<!-- <string>@@STDOUTFILE@@</string> -->
</dict>
</plist>

View file

@ -1,7 +1,9 @@
[Unit]
Description=Synchronize calendars and contacts
Documentation=https://vdirsyncer.readthedocs.org/
StartLimitBurst=2
[Service]
ExecStart=/usr/bin/vdirsyncer sync
Type=oneshot
RuntimeMaxSec=3m
Restart=on-failure

View file

@ -1,18 +0,0 @@
version: '2'
services:
nextcloud:
image: nextcloud
ports:
- '5000:80'
environment:
- SQLITE_DATABASE=nextcloud
- NEXTCLOUD_ADMIN_USER=asdf
- NEXTCLOUD_ADMIN_PASSWORD=asdf
xandikos:
build:
context: .
dockerfile: docker/xandikos/Dockerfile
ports:
- '5001:5001'

View file

@ -1,13 +0,0 @@
# Original file copyright 2017 Jelmer Vernooij
FROM ubuntu:latest
RUN apt-get update && apt-get -y install xandikos locales
EXPOSE 8080
RUN locale-gen en_US.UTF-8
ENV PYTHONIOENCODING=utf-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
CMD xandikos -d /tmp/dav -l 0.0.0.0 -p 5001 --autocreate

View file

@ -1,3 +0,0 @@
sphinx != 1.4.7
sphinx_rtd_theme
setuptools_scm

View file

@ -1 +1 @@
.. include:: ../CHANGELOG.rst
.. include:: ../CHANGELOG.rst

View file

@ -1,93 +1,106 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import datetime
import os
import setuptools_scm
from pkg_resources import get_distribution
extensions = ['sphinx.ext.autodoc']
extensions = ["sphinx.ext.autodoc"]
templates_path = ['_templates']
templates_path = ["_templates"]
source_suffix = '.rst'
master_doc = 'index'
source_suffix = ".rst"
master_doc = "index"
project = u'vdirsyncer'
copyright = (u'2014-{}, Markus Unterwaditzer & contributors'
.format(datetime.date.today().strftime('%Y')))
project = "vdirsyncer"
copyright = "2014-{}, Markus Unterwaditzer & contributors".format(
datetime.date.today().strftime("%Y")
)
release = setuptools_scm.get_version(root='..', relative_to=__file__)
version = '.'.join(release.split('.')[:2]) # The short X.Y version.
release = get_distribution("vdirsyncer").version
version = ".".join(release.split(".")[:2]) # The short X.Y version.
rst_epilog = '.. |vdirsyncer_version| replace:: %s' % release
rst_epilog = f".. |vdirsyncer_version| replace:: {release}"
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
pygments_style = 'sphinx'
pygments_style = "sphinx"
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
try:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
except ImportError:
html_theme = 'default'
html_theme = "default"
if not on_rtd:
print('-' * 74)
print('Warning: sphinx-rtd-theme not installed, building with default '
'theme.')
print('-' * 74)
print("-" * 74)
print("Warning: sphinx-rtd-theme not installed, building with default theme.")
print("-" * 74)
html_static_path = ['_static']
htmlhelp_basename = 'vdirsyncerdoc'
html_static_path = ["_static"]
htmlhelp_basename = "vdirsyncerdoc"
latex_elements = {}
latex_documents = [
('index', 'vdirsyncer.tex', u'vdirsyncer Documentation',
u'Markus Unterwaditzer', 'manual'),
(
"index",
"vdirsyncer.tex",
"vdirsyncer Documentation",
"Markus Unterwaditzer",
"manual",
),
]
man_pages = [
('index', 'vdirsyncer', u'vdirsyncer Documentation',
[u'Markus Unterwaditzer'], 1)
("index", "vdirsyncer", "vdirsyncer Documentation", ["Markus Unterwaditzer"], 1)
]
texinfo_documents = [
('index', 'vdirsyncer', u'vdirsyncer Documentation',
u'Markus Unterwaditzer', 'vdirsyncer',
'Synchronize calendars and contacts.', 'Miscellaneous'),
(
"index",
"vdirsyncer",
"vdirsyncer Documentation",
"Markus Unterwaditzer",
"vdirsyncer",
"Synchronize calendars and contacts.",
"Miscellaneous",
),
]
def github_issue_role(name, rawtext, text, lineno, inliner,
options={}, content=()): # noqa: B006
def github_issue_role(name, rawtext, text, lineno, inliner, options=None, content=()):
options = options or {}
try:
issue_num = int(text)
if issue_num <= 0:
raise ValueError()
raise ValueError
except ValueError:
msg = inliner.reporter.error('Invalid GitHub issue: {}'.format(text),
line=lineno)
msg = inliner.reporter.error(f"Invalid GitHub issue: {text}", line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
from docutils import nodes
PROJECT_HOME = 'https://github.com/pimutils/vdirsyncer'
link = '{}/{}/{}'.format(PROJECT_HOME,
'issues' if name == 'gh' else 'pull',
issue_num)
linktext = ('issue #{}' if name == 'gh'
else 'pull request #{}').format(issue_num)
node = nodes.reference(rawtext, linktext, refuri=link,
**options)
PROJECT_HOME = "https://github.com/pimutils/vdirsyncer"
link = "{}/{}/{}".format(
PROJECT_HOME, "issues" if name == "gh" else "pull", issue_num
)
linktext = ("issue #{}" if name == "gh" else "pull request #{}").format(issue_num)
node = nodes.reference(rawtext, linktext, refuri=link, **options)
return [node], []
def setup(app):
from sphinx.domains.python import PyObject
app.add_object_type('storage', 'storage', 'pair: %s; storage',
doc_field_types=PyObject.doc_field_types)
app.add_role('gh', github_issue_role)
app.add_role('ghpr', github_issue_role)
app.add_object_type(
"storage",
"storage",
"pair: %s; storage",
doc_field_types=PyObject.doc_field_types,
)
app.add_role("gh", github_issue_role)
app.add_role("ghpr", github_issue_role)

View file

@ -61,7 +61,8 @@ Pair Section
sync`` is executed. See also :ref:`collections_tutorial`.
The special values ``"from a"`` and ``"from b"``, tell vdirsyncer to try
autodiscovery on a specific storage.
autodiscovery on a specific storage. It means all the collections on side A /
side B.
If the collection you want to sync doesn't have the same name on each side,
you may also use a value of the form ``["config_name", "name_a", "name_b"]``.
@ -71,8 +72,8 @@ Pair Section
Examples:
- ``collections = ["from b", "foo", "bar"]`` makes vdirsyncer synchronize the
collections from side B, and also the collections named "foo" and "bar".
- ``collections = ["from b", "foo", "bar"]`` makes vdirsyncer synchronize all
the collections from side B, and also the collections named "foo" and "bar".
- ``collections = ["from b", "from a"]`` makes vdirsyncer synchronize all
existing collections on either side.
@ -116,10 +117,26 @@ Pair Section
- ``metadata``: Metadata keys that should be synchronized when ``vdirsyncer
metasync`` is executed. Example::
metadata = ["color", "displayname"]
metadata = ["color", "displayname", "description", "order"]
This synchronizes the ``color`` and the ``displayname`` properties. The
``conflict_resolution`` parameter applies here as well.
This synchronizes the following properties:
- color: ``http://apple.com/ns/ical/:calendar-color``
- displayname: ``DAV:displayname``
- description: ``CalDAV:calendar-description`` and ``CardDAV:addressbook-description``
- order: ``http://apple.com/ns/ical/:calendar-order``
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:
@ -169,7 +186,7 @@ CalDAV and CardDAV
url = "..."
#username = ""
#password = ""
#verify = true
#verify = /path/to/custom_ca.pem
#auth = null
#useragent = "vdirsyncer/0.16.4"
#verify_fingerprint = null
@ -202,12 +219,10 @@ CalDAV and CardDAV
:param url: Base URL or an URL to a calendar.
:param username: Username for authentication.
:param password: Password for authentication.
:param verify: Verify SSL certificate, default True. This can also be a
local path to a self-signed SSL certificate. See :ref:`ssl-tutorial`
for more information.
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the
expected server certificate. See :ref:`ssl-tutorial` for more
information.
:param verify: Optional. Local path to a self-signed SSL certificate.
See :ref:`ssl-tutorial` for more information.
:param verify_fingerprint: Optional. SHA256 fingerprint of the expected
server certificate. See :ref:`ssl-tutorial` for more information.
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
default is preemptive Basic auth, sending credentials even if server
didn't request them. This saves from an additional roundtrip per
@ -229,21 +244,20 @@ CalDAV and CardDAV
url = "..."
#username = ""
#password = ""
#verify = true
#verify = /path/to/custom_ca.pem
#auth = null
#useragent = "vdirsyncer/0.16.4"
#verify_fingerprint = null
#auth_cert = null
#use_vcard_4 = false
:param url: Base URL or an URL to an addressbook.
:param username: Username for authentication.
:param password: Password for authentication.
:param verify: Verify SSL certificate, default True. This can also be a
local path to a self-signed SSL certificate. See
:ref:`ssl-tutorial` for more information.
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the expected
server certificate. See :ref:`ssl-tutorial` for
more information.
:param verify: Optional. Local path to a self-signed SSL certificate.
See :ref:`ssl-tutorial` for more information.
:param verify_fingerprint: Optional. SHA256 fingerprint of the expected
server certificate. See :ref:`ssl-tutorial` for more information.
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
default is preemptive Basic auth, sending credentials even if
server didn't request them. This saves from an additional
@ -253,6 +267,7 @@ CalDAV and CardDAV
certificate and the key or a list of paths to the files
with them.
:param useragent: Default ``vdirsyncer``.
:param use_vcard_4: Whether the server use vCard 4.0.
Google
++++++
@ -266,7 +281,15 @@ in terms of data safety**. See `this blog post
<https://evertpot.com/google-carddav-issues/>`_ for the details. Always back
up your data.
At first run you will be asked to authorize application for google account
Another caveat is that Google group labels are not synced with vCard's
`CATEGORIES <https://www.rfc-editor.org/rfc/rfc6350#section-6.7.1>`_ property
(also see :gh:`814` and
`upstream issue #36761530 <https://issuetracker.google.com/issues/36761530>`_
for reference) and the
`BDAY <https://www.rfc-editor.org/rfc/rfc6350#section-6.2.5>`_ property is not
synced when only partial date information is present (e.g. the year is missing).
At first run you will be asked to authorize application for Google account
access.
To use this storage type, you need to install some additional dependencies::
@ -277,25 +300,29 @@ Furthermore you need to register vdirsyncer as an application yourself to
obtain ``client_id`` and ``client_secret``, as it is against Google's Terms of
Service to hardcode those into opensource software [googleterms]_:
1. Go to the `Google API Manager <https://console.developers.google.com>`_ and
create a new project under any name.
1. Go to the `Google API Manager <https://console.developers.google.com>`_
2. Create a new project under any name.
2. Within that project, enable the "CalDAV" and "CardDAV" APIs (**not** the
Calendar and Contacts APIs, those are different and won't work). There should
be a searchbox where you can just enter those terms.
be a search box where you can just enter those terms.
3. In the sidebar, select "Credentials", then "Create Credentials" and create a
new "OAuth Client ID".
3. In the sidebar, select "Credentials" and create a new "OAuth Client ID". The
application type is "Other".
You'll be prompted to create a OAuth consent screen first. Fill out that
form however you like.
After setting up the consent screen, finish creating the new "OAuth Client
ID'. The correct application type is "Desktop application".
4. Finally you should have a Client ID and a Client secret. Provide these in
your storage config.
The ``token_file`` parameter should be a filepath where vdirsyncer can later
store authentication-related data. You do not need to create the file itself
or write anything to it.
The ``token_file`` parameter should be a path to a file where vdirsyncer can
later store authentication-related data. You do not need to create the file
itself or write anything to it.
.. [googleterms] See `ToS <https://developers.google.com/terms/?hl=th>`_,
section "Confidential Matters".
@ -303,7 +330,7 @@ or write anything to it.
.. note::
You need to configure which calendars Google should offer vdirsyncer using
a rather hidden `settings page
a secret `settings page
<https://calendar.google.com/calendar/syncselect>`_.
.. storage:: google_calendar
@ -343,55 +370,9 @@ or write anything to it.
:param client_id/client_secret: OAuth credentials, obtained from the Google
API Manager.
EteSync
+++++++
`EteSync <https://www.etesync.com/>`_ is a new cloud provider for end to end
encrypted contacts and calendar storage. Vdirsyncer contains **experimental**
support for it.
To use it, you need to install some optional dependencies::
pip install vdirsyncer[etesync]
On first usage you will be prompted for the service password and the encryption
password. Neither are stored.
.. storage:: etesync_contacts
Contacts for etesync.
::
[storage example_for_etesync_contacts]
email = ...
secrets_dir = ...
#server_path = ...
#db_path = ...
:param email: The email address of your account.
:param secrets_dir: A directory where vdirsyncer can store the encryption
key and authentication token.
:param server_url: Optional. URL to the root of your custom server.
:param db_path: Optional. Use a different path for the database.
.. storage:: etesync_calendars
Calendars for etesync.
::
[storage example_for_etesync_calendars]
email = ...
secrets_dir = ...
#server_path = ...
#db_path = ...
:param email: The email address of your account.
:param secrets_dir: A directory where vdirsyncer can store the encryption
key and authentication token.
:param server_url: Optional. URL to the root of your custom server.
:param db_path: Optional. Use a different path for the database.
The current flow is not ideal, but Google has deprecated the previous APIs used
for this without providing a suitable replacement. See :gh:`975` for discussion
on the topic.
Local
+++++
@ -408,6 +389,8 @@ Local
fileext = "..."
#encoding = "utf-8"
#post_hook = null
#pre_deletion_hook = null
#fileignoreext = ".tmp"
Can be used with `khal <http://lostpackets.de/khal/>`_. See :doc:`vdir` for
a more formal description of the format.
@ -421,11 +404,17 @@ Local
:param fileext: The file extension to use (e.g. ``.txt``). Contained in the
href, so if you change the file extension after a sync, this will
trigger a re-download of everything (but *should* not cause data-loss
of any kind).
of any kind). To be compatible with the ``vset`` format you have
to either use ``.vcf`` or ``.ics``. Note that metasync won't work
if you use an empty string here.
:param encoding: File encoding for items, both content and filename.
:param post_hook: A command to call for each item creation and
modification. The command will be called with the path of the
new/updated file.
:param pre_deletion_hook: A command to call for each item deletion.
The command will be called with the path of the deleted file.
:param fileeignoreext: The file extention to ignore. It is only useful
if fileext is set to the empty string. The default is ``.tmp``.
.. storage:: singlefile
@ -505,6 +494,7 @@ leads to an error.
[storage holidays_remote]
type = "http"
url = https://example.com/holidays_from_hicksville.ics
#filter_hook = null
Too many WebCAL providers generate UIDs of all ``VEVENT``-components
on-the-fly, i.e. all UIDs change every time the calendar is downloaded.
@ -515,10 +505,22 @@ leads to an error.
of the normalized item content.
:param url: URL to the ``.ics`` file.
:param username: Username for HTTP basic authentication.
:param password: Password for HTTP basic authentication.
:param useragent: Default ``vdirsyncer``.
:param verify_cert: Add one new root certificate file in PEM format. Useful
for servers with self-signed certificates.
:param username: Username for authentication.
:param password: Password for authentication.
:param verify: Optional. Local path to a self-signed SSL certificate.
See :ref:`ssl-tutorial` for more information.
:param verify_fingerprint: Optional. SHA256 fingerprint of the expected
server certificate. See :ref:`ssl-tutorial` for more information.
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
default is preemptive Basic auth, sending credentials even if server
didn't request them. This saves from an additional roundtrip per
request. Consider setting ``guess`` if this causes issues with your
server.
:param auth_cert: Optional. Either a path to a certificate with a client
certificate and the key or a list of paths to the files with them.
:param useragent: Default ``vdirsyncer``.
:param filter_hook: Optional. A filter command to call for each fetched
item, passed in raw form to stdin and returned via stdout.
If nothing is returned by the filter command, the item is skipped.
This can be used to alter fields as needed when dealing with providers
generating malformed events.

View file

@ -2,14 +2,11 @@
Support and Contact
===================
* The ``#pimutils`` `IRC channel on Freenode <https://pimutils.org/contact>`_
* The ``#pimutils`` `IRC channel on Libera.Chat <https://pimutils.org/contact>`_
might be active, depending on your timezone. Use it for support and general
(including off-topic) discussion.
* Open `a GitHub issue <https://github.com/pimutils/vdirsyncer/issues/>`_ for
concrete bug reports and feature requests.
* Lastly, you can also `contact the author directly
<https://unterwaditzer.net/contact.html>`_. Do this for security issues. If
that doesn't work out (i.e. if I don't respond within one week), use
``contact@pimutils.org``.
* For security issues, contact ``contact@pimutils.org``.

View file

@ -75,36 +75,36 @@ Submitting patches, pull requests
Running tests, how to set up your development environment
---------------------------------------------------------
For many patches, it might suffice to just let Travis run the tests. However,
Travis is slow, so you might want to run them locally too. For this, set up a
For many patches, it might suffice to just let CI run the tests. However,
CI is slow, so you might want to run them locally too. For this, set up a
virtualenv_ and run this inside of it::
# install:
# Install development dependencies, including:
# - vdirsyncer from the repo into the virtualenv
# - stylecheckers (flake8) and code formatters (autopep8)
# - style checks and formatting (ruff)
make install-dev
# Install git commit hook for the stylechecker
make install-git-hooks
# install test dependencies
make install-test
# Install git commit hook for some extra linting and checking
pre-commit install
Then you can run::
make test # The normal testsuite
make style # Stylechecker
make docs # Build the HTML docs, output is at docs/_build/html/
pytest # The normal testsuite
pre-commit run --all # Run all linters (which also run via pre-commit)
make -C docs html # Build the HTML docs, output is at docs/_build/html/
make -C docs linkcheck # Check docs for any broken links
The ``Makefile`` has a lot of options that allow you to control which tests are
run, and which servers are tested. Take a look at its code where they are all
initialized and documented.
For example, to test xandikos, run::
To tests against a specific DAV server, use ``DAV_SERVER``::
make DAV_SERVER=xandikos install-test
make DAV_SERVER=xandikos test
The server will be initialised in a docker container and terminated at the end
of the test suite.
If you have any questions, feel free to open issues about it.
Structure of the testsuite

View file

@ -2,23 +2,14 @@
Donations
=========
vdirsyncer is and will always be free and open source software. We appreciate
sponsors willing to fund our continued work on it.
If you found my work useful, please consider donating. Thank you!
- Bitcoin: ``16sSHxZm263WHR9P9PJjCxp64jp9ooXKVt``
- Bitcoin: ``13p42uWDL62bNRH3KWA6cSpSgvnHy1fs2E``.
- Sponsor via one-time tips or recurring donations `via Ko-fi`_.
- Sponsor via recurring donations `via liberapay`_.
- `PayPal.me <https://www.paypal.me/untitaker>`_
- `Bountysource <https://www.bountysource.com/teams/vdirsyncer>`_ is useful for
funding work on a specific GitHub issue.
- There's also `Bountysource Salt
<https://salt.bountysource.com/teams/vdirsyncer>`_, for one-time and
recurring donations.
- Donations via Bountysource are publicly listed. Use PayPal if you dislike
that.
- `Flattr
<https://flattr.com/submit/auto?user_id=untitaker&url=https%3A%2F%2Fgithub.com%2Fpimutils%2Fvdirsyncer>`_
or `Gratipay <https://gratipay.com/vdirsyncer/>`_ can be used for
recurring donations.
.. _via Ko-fi: https://ko-fi.com/whynothugo
.. _via liberapay: https://liberapay.com/WhyNotHugo/

View file

@ -7,17 +7,18 @@ Installation
OS/distro packages
------------------
The following packages are user-contributed and were up-to-date at the time of
writing:
The following packages are community-contributed and were up-to-date at the
time of writing:
- `ArchLinux <https://www.archlinux.org/packages/community/any/vdirsyncer/>`_
- `Arch Linux <https://archlinux.org/packages/extra/any/vdirsyncer/>`_
- `Ubuntu and Debian, x86_64-only
<https://packagecloud.io/pimutils/vdirsyncer>`_ (packages also exist
in the official repositories but may be out of date)
- `GNU Guix <https://www.gnu.org/software/guix/package-list.html#vdirsyncer>`_
- `OS X (homebrew) <http://braumeister.org/formula/vdirsyncer>`_
- `BSD (pkgsrc) <http://pkgsrc.se/time/py-vdirsyncer>`_
- `GNU Guix <https://packages.guix.gnu.org/packages/vdirsyncer/>`_
- `macOS (homebrew) <https://formulae.brew.sh/formula/vdirsyncer>`_
- `NetBSD <https://ftp.netbsd.org/pub/pkgsrc/current/pkgsrc/time/py-vdirsyncer/index.html>`_
- `OpenBSD <http://ports.su/productivity/vdirsyncer>`_
- `Slackware (SlackBuild at Slackbuilds.org) <https://slackbuilds.org/repository/15.0/network/vdirsyncer/>`_
We only support the latest version of vdirsyncer, which is at the time of this
writing |vdirsyncer_version|. Please **do not file bugs if you use an older
@ -41,37 +42,55 @@ If your distribution doesn't provide a package for vdirsyncer, you still can
use Python's package manager "pip". First, you'll have to check that the
following things are installed:
- Python 3.4+ and pip.
- Python 3.9 to 3.13 and pip.
- ``libxml`` and ``libxslt``
- ``zlib``
- `Rust <https://www.rust-lang.org/>`_, the programming language, together with
its package manager ``cargo``.
- Linux or OS X. **Windows is not supported**, see :gh:`535`.
- Linux or macOS. **Windows is not supported**, see :gh:`535`.
On Linux systems, using the distro's package manager is the best way to do
this, for example, using Ubuntu (last tried on Trusty)::
On Linux systems, using the distro's package manager is the best
way to do this, for example, using Ubuntu::
sudo apt-get install python3 python3-pip libffi-dev
Rust may need to be installed separately, as the packages in Ubuntu are usually
out-of-date. I recommend `rustup <https://rustup.rs/>`_ for that.
sudo apt-get install libxml2 libxslt1.1 zlib1g python3
Then you have several options. The following text applies for most Python
software by the way.
pipx: The clean, easy way
~~~~~~~~~~~~~~~~~~~~~~~~~
pipx_ is a new package manager for Python-based software that automatically
sets up a virtual environment for each program it installs. Please note that
installing via pipx will not include manual pages nor systemd services.
pipx will install vdirsyncer into ``~/.local/pipx/venvs/vdirsyncer``
Assuming that pipx is installed, vdirsyncer can be installed with::
pipx install vdirsyncer
It can later be updated to the latest version with::
pipx upgrade vdirsyncer
And can be uninstalled with::
pipx uninstall vdirsyncer
This last command will remove vdirsyncer and any dependencies installed into
the above location.
.. _pipx: https://github.com/pipxproject/pipx
The dirty, easy way
~~~~~~~~~~~~~~~~~~~
The easiest way to install vdirsyncer at this point would be to run::
If pipx is not available on your distribution, the easiest way to install
vdirsyncer at this point would be to run::
pip3 install -v --user --ignore-installed vdirsyncer
pip install --ignore-installed vdirsyncer
- ``--user`` is to install without root rights (into your home directory)
- ``--ignore-installed`` is to work around Debian's potentially broken packages
(see :ref:`debian-urllib3`). You can try to omit it if you run into other
problems related to certificates, for example.
Your executable is then in ``~/.local/bin/``.
(see :ref:`debian-urllib3`).
This method has a major flaw though: Pip doesn't keep track of the files it
installs. Vdirsyncer's files would be located somewhere in
@ -87,9 +106,9 @@ There is a way to install Python software without scattering stuff across
your filesystem: virtualenv_. There are a lot of resources on how to use it,
the simplest possible way would look something like::
virtualenv --python python3 ~/vdirsyncer_env
~/vdirsyncer_env/bin/pip install -v vdirsyncer
alias vdirsyncer="$HOME/vdirsyncer_env/bin/vdirsyncer"
virtualenv ~/vdirsyncer_env
~/vdirsyncer_env/bin/pip install vdirsyncer
alias vdirsyncer="~/vdirsyncer_env/bin/vdirsyncer"
You'll have to put the last line into your ``.bashrc`` or ``.bash_profile``.
@ -100,25 +119,4 @@ This method has two advantages:
distro-specific issues.
- You can delete ``~/vdirsyncer_env/`` to uninstall vdirsyncer entirely.
The clean, easy way
~~~~~~~~~~~~~~~~~~~
pipsi_ is a new package manager for Python-based software that automatically
sets up a virtualenv for each program you install. Assuming you have it
installed on your operating system, you can do::
pipsi install --python python3 vdirsyncer
and ``.local/bin/vdirsyncer`` will be your new vdirsyncer installation. To
update vdirsyncer to the latest version::
pipsi upgrade vdirsyncer
If you're done with vdirsyncer, you can do::
pipsi uninstall vdirsyncer
and vdirsyncer will be uninstalled, including its dependencies.
.. _virtualenv: https://virtualenv.readthedocs.io/
.. _pipsi: https://github.com/mitsuhiko/pipsi

View file

@ -38,6 +38,12 @@ You can fetch the username as well::
Or really any kind of parameter in a storage section.
You can also pass the command as a string to be executed in a shell::
[storage foo]
...
password.fetch = ["shell", "~/.local/bin/get-my-password | head -n1"]
With pass_ for example, you might find yourself writing something like this in
your configuration file::
@ -60,7 +66,7 @@ passwords from the OS's password store. Installation::
Basic usage::
password.fetch = ["command", "keyring", "get", "example.com", "foouser"]
.. _keyring: https://github.com/jaraco/keyring/
Password Prompt
@ -72,3 +78,19 @@ You can also simply prompt for the password::
type = "caldav"
username = "myusername"
password.fetch = ["prompt", "Password for CalDAV"]
Environment variable
===============
To read the password from an environment variable::
[storage foo]
type = "caldav"
username = "myusername"
password.fetch = ["command", "printenv", "DAV_PW"]
This is especially handy if you use the same password multiple times
(say, for a CardDAV and a CalDAV storage).
On bash, you can read and export the password without printing::
read -s DAV_PW "DAV Password: " && export DAV_PW

View file

@ -5,23 +5,27 @@ Packaging guidelines
Thank you very much for packaging vdirsyncer! The following guidelines should
help you to avoid some common pitfalls.
While they are called guidelines and therefore theoretically not mandatory, if
you consider going a different direction, please first open an issue or contact
me otherwise instead of just going ahead. These guidelines exist for my own
convenience too.
If you find yourself needing to patch anything, or going in a different direction,
please open an issue so we can also address in a way that works for everyone. Otherwise
we get bug reports for code or scenarios that don't exist in upstream vdirsycner.
Obtaining the source code
=========================
The main distribution channel is `PyPI
<https://pypi.python.org/pypi/vdirsyncer>`_, and source tarballs can be
obtained there. Do not use the ones from GitHub: Their tarballs contain useless
junk and are more of a distraction than anything else.
obtained there. We mirror the same package tarball and wheel as GitHub
releases. Please do not confuse these with the auto-generated GitHub "Source
Code" tarball. Those are missing some important metadata and your build will fail.
I give each release a tag in the git repo. If you want to get notified of new
We give each release a tag in the git repo. If you want to get notified of new
releases, `GitHub's feed
<https://github.com/pimutils/vdirsyncer/releases.atom>`_ is a good way.
Tags will be signed by the maintainer who is doing the release (starting with
0.16.8), and generation of the tarball and wheel is done by CI. Hence, only the
tag itself is signed.
Dependency versions
===================
@ -33,24 +37,25 @@ Testing
=======
Everything testing-related goes through the ``Makefile`` in the root of the
repository or PyPI package. Trying to e.g. run ``py.test`` directly will
repository or PyPI package. Trying to e.g. run ``pytest`` directly will
require a lot of environment variables to be set (for configuration) and you
probably don't want to deal with that.
You can install the testing dependencies with::
You can install the all development dependencies with::
make install-test
make install-dev
You probably don't want this since it will use pip to download the
dependencies. Alternatively you can find the testing dependencies in
``test-requirements.txt``, again with lower-bound version requirements.
dependencies. Alternatively test dependencies are listed as ``test`` optional
dependencies in ``pyproject.toml``, again with lower-bound version
requirements.
You also have to have vdirsyncer fully installed at this point. Merely
``cd``-ing into the tarball will not be sufficient.
Running the tests happens with::
make test
pytest
Hypothesis will randomly generate test input. If you care about deterministic
tests, set the ``DETERMINISTIC_TESTS`` variable to ``"true"``::
@ -69,10 +74,11 @@ Using Sphinx_ you can generate the documentation you're reading right now in a
variety of formats, such as HTML, PDF, or even as a manpage. That said, I only
take care of the HTML docs' formatting.
You can find a list of dependencies in ``docs-requirements.txt``. Again, you
can install those using pip with::
You can find a list of dependencies in ``pyproject.toml``, in the
``project.optional-dependencies`` section as ``docs``. Again, you can install
those using pip with::
make install-docs
pip install '.[docs]'
Then change into the ``docs/`` directory and build whatever format you want
using the ``Makefile`` in there (run ``make`` for the formats you can build).

View file

@ -66,3 +66,7 @@ For such purposes you can set the ``partial_sync`` parameter to ``ignore``::
partial_sync = ignore
See :ref:`the config docs <partial_sync_def>` for more information.
.. _nextCloud: https://nextcloud.com/
.. _Baikal: http://sabre.io/baikal/
.. _DAViCal: http://www.davical.org/

View file

@ -18,5 +18,5 @@ package that don't play well with packages assuming a normal ``requests``. This
is due to stubbornness on both sides.
See :gh:`82` and :gh:`140` for past discussions. You have one option to work
around this, that is, to install vdirsyncer in a virtualenv, see
around this, that is, to install vdirsyncer in a virtual environment, see
:ref:`manual-installation`.

View file

@ -14,21 +14,14 @@ To pin the certificate by fingerprint::
[storage foo]
type = "caldav"
...
verify_fingerprint = "94:FD:7A:CB:50:75:A4:69:82:0A:F8:23:DF:07:FC:69:3E:CD:90:CA"
#verify = false # Optional: Disable CA validation, useful for self-signed certs
verify_fingerprint = "6D:83:EA:32:6C:39:BA:08:ED:EB:C9:BC:BE:12:BB:BF:0F:D9:83:00:CC:89:7E:C7:32:05:94:96:CA:C5:59:5E"
SHA1-, SHA256- or MD5-Fingerprints can be used. They're detected by their
length.
SHA256-Fingerprints must be used, MD5 and SHA-1 are insecure and not supported.
CA validation is disabled when pinning a fingerprint.
You can use the following command for obtaining a SHA-1 fingerprint::
You can use the following command for obtaining a SHA256 fingerprint::
echo -n | openssl s_client -connect unterwaditzer.net:443 | openssl x509 -noout -fingerprint
Note that ``verify_fingerprint`` doesn't suffice for vdirsyncer to work with
self-signed certificates (or certificates that are not in your trust store). You
most likely need to set ``verify = false`` as well. This disables verification
of the SSL certificate's expiration time and the existence of it in your trust
store, all that's verified now is the fingerprint.
echo -n | openssl s_client -connect unterwaditzer.net:443 | openssl x509 -noout -fingerprint -sha256
However, please consider using `Let's Encrypt <https://letsencrypt.org/>`_ such
that you can forget about all of that. It is easier to deploy a free
@ -47,22 +40,16 @@ To point vdirsyncer to a custom set of root CAs::
...
verify = "/path/to/cert.pem"
Vdirsyncer uses the requests_ library, which, by default, `uses its own set of
trusted CAs
<http://www.python-requests.org/en/latest/user/advanced/#ca-certificates>`_.
Vdirsyncer uses the aiohttp_ library, which uses the default `ssl.SSLContext
https://docs.python.org/3/library/ssl.html#ssl.SSLContext`_ by default.
However, the actual behavior depends on how you have installed it. Many Linux
distributions patch their ``python-requests`` package to use the system
certificate CAs. Normally these two stores are similar enough for you to not
care.
There are cases where certificate validation fails even though you can access
the server fine through e.g. your browser. This usually indicates that your
installation of ``python`` or the ``aiohttp`` or library is somehow broken. In
such cases, it makes sense to explicitly set ``verify`` or
``verify_fingerprint`` as shown above.
But there are cases where certificate validation fails even though you can
access the server fine through e.g. your browser. This usually indicates that
your installation of the ``requests`` library is somehow broken. In such cases,
it makes sense to explicitly set ``verify`` or ``verify_fingerprint`` as shown
above.
.. _requests: http://www.python-requests.org/
.. _aiohttp: https://docs.aiohttp.org/en/stable/index.html
.. _ssl-client-certs:

View file

@ -16,7 +16,7 @@ Configuration
.. note::
- The `config.example from the repository
<https://github.com/pimutils/vdirsyncer/blob/master/config.example>`_
<https://github.com/pimutils/vdirsyncer/blob/main/config.example>`_
contains a very terse version of this.
- In this example we set up contacts synchronization, but calendar sync
@ -53,8 +53,7 @@ pairs of storages should actually be synchronized is defined in :ref:`pair
section <pair_config>`. This format is copied from OfflineIMAP, where storages
are called repositories and pairs are called accounts.
The following example synchronizes addressbooks from a :doc:`NextCloud
<tutorials/nextcloud>` to ``~/.contacts/``::
The following example synchronizes ownCloud's addressbooks to ``~/.contacts/``::
[pair my_contacts]
@ -71,7 +70,7 @@ The following example synchronizes addressbooks from a :doc:`NextCloud
type = "carddav"
# We can simplify this URL here as well. In theory it shouldn't matter.
url = "https://nextcloud.example.com/"
url = "https://owncloud.example.com/remote.php/carddav/"
username = "bob"
password = "asdf"
@ -163,13 +162,13 @@ let's switch to a different base example. This time we'll synchronize calendars:
[storage my_calendars_remote]
type = "caldav"
url = "https://nextcloud.example.com/"
url = "https://owncloud.example.com/remote.php/caldav/"
username = "bob"
password = "asdf"
Run ``vdirsyncer discover`` for discovery. Then you can use ``vdirsyncer
metasync`` to synchronize the ``color`` property between your local calendars
in ``~/.calendars/`` and your NextCloud. Locally the color is just represented
in ``~/.calendars/`` and your ownCloud. Locally the color is just represented
as a file called ``color`` within the calendar folder.
.. _collections_tutorial:
@ -177,8 +176,11 @@ as a file called ``color`` within the calendar folder.
More information about collections
----------------------------------
"Collection" is a collective term for addressbooks and calendars. Each
collection from a storage has a "collection name", a unique identifier for each
"Collection" is a collective term for addressbooks and calendars. A Cardav or
Caldav server can contains several "collections" which correspond to several
addressbooks or calendar.
Each collection from a storage has a "collection name", a unique identifier for each
collection. In the case of :storage:`filesystem`-storage, this is the name of the
directory that represents the collection, in the case of the DAV-storages this
is the last segment of the URL. We use this identifier in the ``collections``

10
docs/tutorials/baikal.rst Normal file
View file

@ -0,0 +1,10 @@
======
Baikal
======
Vdirsyncer is continuously tested against the latest version of Baikal_.
- Baikal up to ``0.2.7`` also uses an old version of SabreDAV, with the same
issue as ownCloud, see :gh:`160`. This issue is fixed in later versions.
.. _Baikal: http://sabre.io/baikal/

View file

@ -52,7 +52,7 @@ this:
setup. We also set the storage to read-only such that no changes get
synchronized back. Claws-Mail should not be able to do any changes anyway,
but this is one extra safety step in case files get corrupted or vdirsyncer
behaves eratically. You can leave that part out if you want to be able to
behaves erratically. You can leave that part out if you want to be able to
edit those files locally.
- In the last section we configure that online contacts win in a conflict
situation. Configure this part however you like. A correct value depends on
@ -69,7 +69,7 @@ Now we discover and sync our contacts::
Claws Mail
----------
Open Claws-Mail. Got to **Tools** => **Addressbook**.
Open Claws-Mail. Go to **Tools** => **Addressbook**.
Click on **Addressbook** => **New vCard**. Choose a name for the book.
@ -77,7 +77,7 @@ Then search for the for the vCard in the folder **~/.contacts/**. Click
ok, and you we will see your contacts.
.. note::
Claws-Mail shows only contacts that have a mail address.
Crontab

View file

@ -10,13 +10,13 @@ the settings to use::
[storage cal]
type = "caldav"
url = "https://caldav.messagingengine.com/"
url = "https://caldav.fastmail.com/"
username = "..."
password = "..."
[storage card]
type = "carddav"
url = "https://carddav.messagingengine.com/"
url = "https://carddav.fastmail.com/"
username = "..."
password = "..."

View file

@ -37,7 +37,7 @@ Further applications, with missing pages:
.. _khal: http://lostpackets.de/khal/
.. _dayplanner: http://www.day-planner.org/
.. _Orage: http://www.kolumbus.fi/~w408237/orage/
.. _Orage: https://gitlab.xfce.org/apps/orage
.. _rainlendar: http://www.rainlendar.net/
.. _khard: https://github.com/scheibler/khard/
.. _contactquery.c: https://github.com/t-8ch/snippets/blob/master/contactquery.c
@ -52,10 +52,12 @@ Servers
.. toctree::
:maxdepth: 1
baikal
davmail
fastmail
google
icloud
nextcloud
owncloud
radicale
xandikos

View file

@ -1,8 +1,8 @@
=========
NextCloud
nextCloud
=========
Vdirsyncer is continuously tested against the latest version of NextCloud_::
Vdirsyncer is continuously tested against the latest version of nextCloud_::
[storage cal]
type = "caldav"
@ -17,4 +17,4 @@ Vdirsyncer is continuously tested against the latest version of NextCloud_::
- WebCAL-subscriptions can't be discovered by vdirsyncer. See `this relevant
issue <https://github.com/nextcloud/calendar/issues/63>`_.
.. _NextCloud: https://nextcloud.com/
.. _nextCloud: https://nextcloud.com/

View file

@ -0,0 +1,26 @@
.. _owncloud_setup:
========
ownCloud
========
Vdirsyncer is continuously tested against the latest version of ownCloud_::
[storage cal]
type = "caldav"
url = "https://example.com/remote.php/dav/"
username = ...
password = ...
[storage card]
type = "carddav"
url = "https://example.com/remote.php/dav/"
username = ...
password = ...
- *Versions older than 7.0.0:* ownCloud uses SabreDAV, which had problems
detecting collisions and race-conditions. The problems were reported and are
fixed in SabreDAV's repo, and the corresponding fix is also in ownCloud since
7.0.0. See :gh:`16` for more information.
.. _ownCloud: https://owncloud.org/

View file

@ -13,8 +13,8 @@ minutes).
unit files, you'll need to download vdirsyncer.service_ and vdirsyncer.timer_
into either ``/etc/systemd/user/`` or ``~/.local/share/systemd/user``.
.. _vdirsyncer.service: https://raw.githubusercontent.com/pimutils/vdirsyncer/master/contrib/vdirsyncer.service
.. _vdirsyncer.timer: https://raw.githubusercontent.com/pimutils/vdirsyncer/master/contrib/vdirsyncer.timer
.. _vdirsyncer.service: https://raw.githubusercontent.com/pimutils/vdirsyncer/main/contrib/vdirsyncer.service
.. _vdirsyncer.timer: https://raw.githubusercontent.com/pimutils/vdirsyncer/main/contrib/vdirsyncer.timer
Activation
----------
@ -29,7 +29,7 @@ It's quite possible that the default "every fifteen minutes" interval isn't to
your liking. No default will suit everybody, but this is configurable by simply
running::
systemctl --user edit vdirsyncer
systemctl --user edit vdirsyncer.timer
This will open a blank editor, where you can override the timer by including::

View file

@ -48,10 +48,9 @@ instance to subfolders of ``~/.calendar/``.
Setting up todoman
==================
Write this to ``~/.config/todoman/todoman.conf``::
Write this to ``~/.config/todoman/config.py``::
[main]
path = ~/.calendars/*
path = "~/.calendars/*"
The glob_ pattern in ``path`` will match all subfolders in ``~/.calendars/``,
which is exactly the tasklists we want. Now you can use ``todoman`` as

View file

@ -56,8 +56,11 @@ have any file extensions.
known from CSS, for example) are allowed. The prefixing ``#`` must be
present.
- A file called ``displayname`` contains a UTF-8 encoded label that may be used
to represent the vdir in UIs.
- Files called ``displayname`` and ``description`` contain a UTF-8 encoded label/
description, that may be used to represent the vdir in UIs.
- A file called ``order`` inside the vdir includes the relative order
of the calendar, a property that is only relevant in UI design.
Writing to vdirs
================
@ -96,7 +99,7 @@ collections for faster search and lookup.
The reason items' filenames don't contain any extra information is simple: The
solutions presented induced duplication of data, where one duplicate might
become out of date because of bad implementations. As it stands right now, a
become out of date because of bad implementations. As it stands right now, an
index format could be formalized separately though.
vdirsyncer doesn't really have to bother about efficient item lookup, because

View file

@ -39,7 +39,7 @@ program chosen:
* Like with ``todo.txt``, Dropbox and friends are obviously agnostic/unaware of
the files' contents. If a file has changed on both sides, Dropbox just copies
both versions to both sides.
This is a good idea if the user is directly interfacing with the file system
and is able to resolve conflicts themselves. Here it might lead to
erroneous behavior with e.g. ``khal``, since there are now two events with
@ -50,7 +50,6 @@ program chosen:
* Such a setup doesn't work at all with smartphones. Vdirsyncer, on the other
hand, synchronizes with CardDAV/CalDAV servers, which can be accessed with
e.g. DAVDroid_ or the apps by dmfs_.
e.g. DAVx⁵_ or other apps bundled with smartphones.
.. _DAVDroid: http://davdroid.bitfire.at/
.. _dmfs: https://dmfs.org/
.. _DAVx⁵: https://www.davx5.com/

29
publish-release.yaml Normal file
View file

@ -0,0 +1,29 @@
# Push new version to PyPI.
#
# Usage: hut builds submit publish-release.yaml --follow
image: alpine/edge
packages:
- py3-build
- py3-pip
- py3-setuptools
- py3-setuptools_scm
- py3-wheel
- twine
sources:
- https://github.com/pimutils/vdirsyncer
secrets:
- a36c8ba3-fba0-4338-b402-6aea0fbe771e # PyPI token.
environment:
CI: true
tasks:
- check-tag: |
cd vdirsyncer
git fetch --tags
# Stop here unless this is a tag.
git describe --exact-match --tags || complete-build
- publish: |
cd vdirsyncer
python -m build --no-isolation
twine upload --non-interactive dist/*

114
pyproject.toml Normal file
View file

@ -0,0 +1,114 @@
# Vdirsyncer synchronizes calendars and contacts.
#
# Please refer to https://vdirsyncer.pimutils.org/en/stable/packaging.html for
# how to package vdirsyncer.
[build-system]
requires = ["setuptools>=64", "setuptools_scm>=8"]
build-backend = "setuptools.build_meta"
[project]
name = "vdirsyncer"
authors = [
{name = "Markus Unterwaditzer", email = "markus@unterwaditzer.net"},
]
description = "Synchronize calendars and contacts"
readme = "README.rst"
requires-python = ">=3.9"
keywords = ["todo", "task", "icalendar", "cli"]
license = "BSD-3-Clause"
license-files = ["LICENSE"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.9",
"Topic :: Internet",
"Topic :: Office/Business :: Scheduling",
"Topic :: Utilities",
]
dependencies = [
"click>=5.0,<9.0",
"click-log>=0.3.0,<0.5.0",
"requests>=2.20.0",
"aiohttp>=3.8.2,<4.0.0",
"aiostream>=0.4.3,<0.8.0",
"tenacity>=9.0.0",
]
dynamic = ["version"]
[project.optional-dependencies]
google = ["aiohttp-oauthlib"]
test = [
"hypothesis>=6.72.0,<7.0.0",
"pytest",
"pytest-cov",
"pytest-httpserver",
"trustme",
"pytest-asyncio",
"aioresponses",
]
docs = [
"sphinx!=1.4.7",
"sphinx_rtd_theme",
"setuptools_scm",
]
check = [
"mypy",
"ruff",
"types-docutils",
"types-requests",
"types-setuptools",
]
[project.scripts]
vdirsyncer = "vdirsyncer.cli:app"
[tool.ruff.lint]
extend-select = [
"B0",
"C4",
"E",
"I",
"RSE",
"SIM",
"TID",
"UP",
"W",
]
[tool.ruff.lint.isort]
force-single-line = true
required-imports = ["from __future__ import annotations"]
[tool.pytest.ini_options]
addopts = """
--tb=short
--cov-config .coveragerc
--cov=vdirsyncer
--cov-report=term-missing:skip-covered
--no-cov-on-fail
--color=yes
"""
# filterwarnings=error
asyncio_default_fixture_loop_scope = "function"
[tool.mypy]
ignore_missing_imports = true
[tool.coverage.report]
exclude_lines = [
"if TYPE_CHECKING:",
]
[tool.setuptools.packages.find]
include = ["vdirsyncer*"]
[tool.setuptools_scm]
write_to = "vdirsyncer/version.py"
version_scheme = "no-guess-dev"

1
rust/.gitignore vendored
View file

@ -1 +0,0 @@
target/

1493
rust/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +0,0 @@
[package]
name = "vdirsyncer-rustext"
version = "0.1.0"
authors = ["Markus Unterwaditzer <markus@unterwaditzer.net>"]
[lib]
name = "vdirsyncer_rustext"
crate-type = ["cdylib"]
[dependencies]
vobject = "0.4.2"
sha2 = "0.7.0"
failure = "0.1"
shippai = "0.2.3"
atomicwrites = "0.2.0"
uuid = { version = "0.6", features = ["v4"] }
libc = "0.2"
log = "0.4"
reqwest = "0.8"
quick-xml = "0.12.0"
url = "1.7"
chrono = "0.4.0"
env_logger = "0.5"

View file

@ -1,4 +0,0 @@
language = "C"
[parse]
expand = ["vdirsyncer-rustext"]

View file

@ -1,59 +0,0 @@
use failure;
pub type Fallible<T> = Result<T, failure::Error>;
shippai_export!();
#[derive(Debug, Fail, Shippai)]
pub enum Error {
#[fail(display = "The item cannot be parsed")]
ItemUnparseable,
#[fail(display = "Unexpected version {}, expected {}", found, expected)]
UnexpectedVobjectVersion { found: String, expected: String },
#[fail(display = "Unexpected component {}, expected {}", found, expected)]
UnexpectedVobject { found: String, expected: String },
#[fail(display = "Item '{}' not found", href)]
ItemNotFound { href: String },
#[fail(display = "The href '{}' is already taken", href)]
ItemAlreadyExisting { href: String },
#[fail(
display = "A wrong etag for '{}' was provided. Another client's requests might \
conflict with vdirsyncer.",
href
)]
WrongEtag { href: String },
#[fail(
display = "The mtime for '{}' has unexpectedly changed. Please close other programs\
accessing this file.",
filepath
)]
MtimeMismatch { filepath: String },
#[fail(
display = "The item '{}' has been rejected by the server because the vobject type was unexpected",
href
)]
UnsupportedVobject { href: String },
#[fail(display = "This storage is read-only.")]
ReadOnly,
}
pub unsafe fn export_result<V>(
res: Result<V, failure::Error>,
c_err: *mut *mut ShippaiError,
) -> Option<V> {
match res {
Ok(v) => Some(v),
Err(e) => {
*c_err = Box::into_raw(Box::new(e.into()));
None
}
}
}

View file

@ -1,256 +0,0 @@
use vobject;
use sha2::{Digest, Sha256};
use std::fmt::Write;
use errors::*;
#[derive(Clone)]
pub enum Item {
Parsed(vobject::Component),
Unparseable(String), // FIXME: maybe use https://crates.io/crates/terminated
}
impl Item {
pub fn from_raw(raw: String) -> Self {
match vobject::parse_component(&raw) {
Ok(x) => Item::Parsed(x),
// Don't chain vobject error here because it cannot be stored/cloned FIXME
_ => Item::Unparseable(raw),
}
}
pub fn from_component(component: vobject::Component) -> Self {
Item::Parsed(component)
}
/// Global identifier of the item, across storages, doesn't change after a modification of the
/// item.
pub fn get_uid(&self) -> Option<String> {
// FIXME: Cache
if let Item::Parsed(ref c) = *self {
let mut stack: Vec<&vobject::Component> = vec![c];
while let Some(vobj) = stack.pop() {
if let Some(prop) = vobj.get_only("UID") {
return Some(prop.value_as_string());
}
stack.extend(vobj.subcomponents.iter());
}
}
None
}
pub fn with_uid(&self, uid: &str) -> Fallible<Self> {
if let Item::Parsed(ref component) = *self {
let mut new_component = component.clone();
change_uid(&mut new_component, uid);
Ok(Item::from_raw(vobject::write_component(&new_component)))
} else {
Err(Error::ItemUnparseable.into())
}
}
/// Raw unvalidated content of the item
pub fn get_raw(&self) -> String {
match *self {
Item::Parsed(ref component) => vobject::write_component(component),
Item::Unparseable(ref x) => x.to_owned(),
}
}
/// Component of item if parseable
pub fn get_component(&self) -> Fallible<&vobject::Component> {
match *self {
Item::Parsed(ref component) => Ok(component),
_ => Err(Error::ItemUnparseable.into()),
}
}
/// Component of item if parseable
pub fn into_component(self) -> Fallible<vobject::Component> {
match self {
Item::Parsed(component) => Ok(component),
_ => Err(Error::ItemUnparseable.into()),
}
}
/// Used for etags
pub fn get_hash(&self) -> Fallible<String> {
// FIXME: cache
if let Item::Parsed(ref component) = *self {
Ok(hash_component(component))
} else {
Err(Error::ItemUnparseable.into())
}
}
/// Used for generating hrefs and matching up items during synchronization. This is either the
/// UID or the hash of the item's content.
pub fn get_ident(&self) -> Fallible<String> {
if let Some(x) = self.get_uid() {
return Ok(x);
}
// We hash the item instead of directly using its raw content, because
// 1. The raw content might be really large, e.g. when it's a contact
// with a picture, which bloats the status file.
//
// 2. The status file would contain really sensitive information.
self.get_hash()
}
pub fn is_parseable(&self) -> bool {
if let Item::Parsed(_) = *self {
true
} else {
false
}
}
}
fn change_uid(c: &mut vobject::Component, uid: &str) {
let mut stack = vec![c];
while let Some(component) = stack.pop() {
match component.name.as_ref() {
"VEVENT" | "VTODO" | "VJOURNAL" | "VCARD" => {
if !uid.is_empty() {
component.set(vobject::Property::new("UID", uid));
} else {
component.remove("UID");
}
}
_ => (),
}
stack.extend(component.subcomponents.iter_mut());
}
}
fn hash_component(c: &vobject::Component) -> String {
let mut new_c = c.clone();
{
let mut stack = vec![&mut new_c];
while let Some(component) = stack.pop() {
// PRODID is changed by radicale for some reason after upload
component.remove("PRODID");
// Sometimes METHOD:PUBLISH is added by WebCAL providers, for us it doesn't make a difference
component.remove("METHOD");
// X-RADICALE-NAME is used by radicale, because hrefs don't really exist in their filesystem backend
component.remove("X-RADICALE-NAME");
// Those are from the VCARD specification and is supposed to change when the
// item does -- however, we can determine that ourselves
component.remove("REV");
component.remove("LAST-MODIFIED");
component.remove("CREATED");
// Some iCalendar HTTP calendars generate the DTSTAMP at request time, so
// this property always changes when the rest of the item didn't. Some do
// the same with the UID.
//
// - Google's read-only calendar links
// - http://www.feiertage-oesterreich.at/
component.remove("DTSTAMP");
component.remove("UID");
if component.name == "VCALENDAR" {
// CALSCALE's default value is gregorian
let calscale = component.get_only("CALSCALE").map(|x| x.value_as_string());
if let Some(x) = calscale {
if x == "GREGORIAN" {
component.remove("CALSCALE");
}
}
// Apparently this is set by Horde?
// https://github.com/pimutils/vdirsyncer/issues/318
// Also Google sets those properties
component.remove("X-WR-CALNAME");
component.remove("X-WR-TIMEZONE");
component.subcomponents.retain(|c| c.name != "VTIMEZONE");
}
stack.extend(component.subcomponents.iter_mut());
}
}
// FIXME: Possible optimization: Stream component to hasher instead of allocating new string
let raw = vobject::write_component(&new_c);
let mut lines: Vec<_> = raw.lines().collect();
lines.sort();
let mut hasher = Sha256::default();
hasher.input(lines.join("\r\n").as_bytes());
let digest = hasher.result();
let mut rv = String::new();
for &byte in digest.as_ref() {
write!(&mut rv, "{:x}", byte).unwrap();
}
rv
}
pub mod exports {
use super::Item;
use errors::*;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
const EMPTY_STRING: *const c_char = b"\0" as *const u8 as *const c_char;
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_get_uid(c: *mut Item) -> *const c_char {
match (*c).get_uid() {
Some(x) => CString::new(x).unwrap().into_raw(),
None => EMPTY_STRING,
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_get_raw(c: *mut Item) -> *const c_char {
CString::new((*c).get_raw()).unwrap().into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_item_from_raw(s: *const c_char) -> *mut Item {
let cstring = CStr::from_ptr(s);
Box::into_raw(Box::new(Item::from_raw(
cstring.to_str().unwrap().to_owned(),
)))
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_item(c: *mut Item) {
let _: Box<Item> = Box::from_raw(c);
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_with_uid(
c: *mut Item,
uid: *const c_char,
err: *mut *mut ShippaiError,
) -> *mut Item {
let uid_cstring = CStr::from_ptr(uid);
if let Some(x) = export_result((*c).with_uid(uid_cstring.to_str().unwrap()), err) {
Box::into_raw(Box::new(x))
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_get_hash(
c: *mut Item,
err: *mut *mut ShippaiError,
) -> *const c_char {
if let Some(x) = export_result((*c).get_hash(), err) {
CString::new(x).unwrap().into_raw()
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_item_is_parseable(c: *mut Item) -> bool {
(*c).is_parseable()
}
}

View file

@ -1,40 +0,0 @@
#![cfg_attr(feature = "cargo-clippy", allow(single_match))]
extern crate atomicwrites;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate shippai;
extern crate libc;
extern crate uuid;
extern crate vobject;
#[macro_use]
extern crate log;
extern crate chrono;
extern crate env_logger;
extern crate quick_xml;
extern crate reqwest;
extern crate sha2;
extern crate url;
pub mod errors;
mod item;
mod storage;
pub mod exports {
use std::ffi::CStr;
use std::os::raw::c_char;
pub use super::item::exports::*;
pub use super::storage::exports::*;
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_str(s: *const c_char) {
CStr::from_ptr(s);
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_init_logger() {
::env_logger::init();
}
}

View file

@ -1,465 +0,0 @@
mod parser;
use chrono;
use std::collections::BTreeSet;
use std::io::{BufReader, Read};
use std::str::FromStr;
use quick_xml;
use reqwest;
use reqwest::header::{ContentType, ETag, EntityTag, IfMatch, IfNoneMatch};
use url::Url;
use super::http::{handle_http_error, send_request, HttpConfig};
use super::utils::generate_href;
use super::Storage;
use errors::*;
use item::Item;
#[inline]
fn propfind() -> reqwest::Method {
reqwest::Method::Extension("PROPFIND".to_owned())
}
#[inline]
fn report() -> reqwest::Method {
reqwest::Method::Extension("REPORT".to_owned())
}
static CALDAV_DT_FORMAT: &'static str = "%Y%m%dT%H%M%SZ";
struct DavStorage {
pub url: String,
pub http_config: HttpConfig,
pub http: Option<reqwest::Client>,
}
impl DavStorage {
pub fn new(url: &str, http_config: HttpConfig) -> Self {
DavStorage {
url: format!("{}/", url.trim_right_matches('/')),
http_config,
http: None,
}
}
}
impl DavStorage {
#[inline]
pub fn get_http(&mut self) -> Fallible<reqwest::Client> {
if let Some(ref http) = self.http {
return Ok(http.clone());
}
let client = self.http_config.clone().into_connection()?.build()?;
self.http = Some(client.clone());
Ok(client)
}
#[inline]
pub fn send_request(&mut self, request: reqwest::Request) -> Fallible<reqwest::Response> {
let url = request.url().to_string();
handle_http_error(&url, send_request(&self.get_http()?, request)?)
}
pub fn get(&mut self, href: &str) -> Fallible<(Item, String)> {
let base = Url::parse(&self.url)?;
let url = base.join(href)?;
if href != url.path() {
Err(Error::ItemNotFound {
href: href.to_owned(),
})?;
}
let request = self.get_http()?.get(url).build()?;
let mut response = self.send_request(request)?;
let mut s = String::new();
response.read_to_string(&mut s)?;
let etag = match response.headers().get::<ETag>() {
Some(x) => format!("\"{}\"", x.tag()),
None => Err(DavError::EtagNotFound)?,
};
Ok((Item::from_raw(s), etag))
}
pub fn list<'a>(
&'a mut self,
mimetype_contains: &'a str,
) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>> {
let mut headers = reqwest::header::Headers::new();
headers.set(ContentType::xml());
headers.set_raw("Depth", "1");
let request = self
.get_http()?
.request(propfind(), &self.url)
.headers(headers)
.body(
r#"<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:resourcetype/>
<D:getcontenttype/>
<D:getetag/>
</D:prop>
</D:propfind>"#,
)
.build()?;
let response = self.send_request(request)?;
self.parse_prop_response(response, mimetype_contains)
}
fn parse_prop_response<'a>(
&'a mut self,
response: reqwest::Response,
mimetype_contains: &'a str,
) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>> {
let buf_reader = BufReader::new(response);
let xml_reader = quick_xml::Reader::from_reader(buf_reader);
let mut parser = parser::ListingParser::new(xml_reader);
let base = Url::parse(&self.url)?;
let mut seen_hrefs = BTreeSet::new();
Ok(Box::new(
parser
.get_all_responses()?
.into_iter()
.filter_map(move |response| {
if response.has_collection_tag {
return None;
}
if !response.mimetype?.contains(mimetype_contains) {
return None;
}
let href = base.join(&response.href?).ok()?.path().to_owned();
if seen_hrefs.contains(&href) {
return None;
}
seen_hrefs.insert(href.clone());
Some((href, response.etag?))
}),
))
}
fn put(
&mut self,
href: &str,
item: &Item,
mimetype: &str,
etag: Option<&str>,
) -> Fallible<(String, String)> {
let base = Url::parse(&self.url)?;
let url = base.join(href)?;
let mut request = self.get_http()?.request(reqwest::Method::Put, url);
request.header(ContentType(reqwest::mime::Mime::from_str(mimetype)?));
if let Some(etag) = etag {
request.header(IfMatch::Items(vec![EntityTag::new(
false,
etag.trim_matches('"').to_owned(),
)]));
} else {
request.header(IfNoneMatch::Any);
}
let raw = item.get_raw();
let response = send_request(&self.get_http()?, request.body(raw).build()?)?;
match (etag, response.status()) {
(Some(_), reqwest::StatusCode::PreconditionFailed) => Err(Error::WrongEtag {
href: href.to_owned(),
})?,
(None, reqwest::StatusCode::PreconditionFailed) => Err(Error::ItemAlreadyExisting {
href: href.to_owned(),
})?,
_ => (),
}
let response = assert_multistatus_success(handle_http_error(href, response)?)?;
// The server may not return an etag under certain conditions:
//
// An origin server MUST NOT send a validator header field (Section
// 7.2), such as an ETag or Last-Modified field, in a successful
// response to PUT unless the request's representation data was saved
// without any transformation applied to the body (i.e., the
// resource's new representation data is identical to the
// representation data received in the PUT request) and the validator
// field value reflects the new representation.
//
// -- https://tools.ietf.org/html/rfc7231#section-4.3.4
//
// In such cases we return a constant etag. The next synchronization
// will then detect an etag change and will download the new item.
let etag = match response.headers().get::<ETag>() {
Some(x) => format!("\"{}\"", x.tag()),
None => "".to_owned(),
};
Ok((response.url().path().to_owned(), etag))
}
fn delete(&mut self, href: &str, etag: &str) -> Fallible<()> {
let base = Url::parse(&self.url)?;
let url = base.join(href)?;
let request = self
.get_http()?
.request(reqwest::Method::Delete, url)
.header(IfMatch::Items(vec![EntityTag::new(
false,
etag.trim_matches('"').to_owned(),
)]))
.build()?;
let response = send_request(&self.get_http()?, request)?;
if response.status() == reqwest::StatusCode::PreconditionFailed {
Err(Error::WrongEtag {
href: href.to_owned(),
})?;
}
assert_multistatus_success(handle_http_error(href, response)?)?;
Ok(())
}
}
fn assert_multistatus_success(r: reqwest::Response) -> Fallible<reqwest::Response> {
// TODO
Ok(r)
}
struct CarddavStorage {
inner: DavStorage,
}
impl CarddavStorage {
pub fn new(url: &str, http_config: HttpConfig) -> Self {
CarddavStorage {
inner: DavStorage::new(url, http_config),
}
}
}
impl Storage for CarddavStorage {
fn list<'a>(&'a mut self) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>> {
self.inner.list("vcard")
}
fn get(&mut self, href: &str) -> Fallible<(Item, String)> {
self.inner.get(href)
}
fn upload(&mut self, item: Item) -> Fallible<(String, String)> {
let href = format!("{}.vcf", generate_href(&item.get_ident()?));
self.inner.put(&href, &item, "text/vcard", None)
}
fn update(&mut self, href: &str, item: Item, etag: &str) -> Fallible<String> {
self.inner
.put(&href, &item, "text/vcard", Some(etag))
.map(|x| x.1)
}
fn delete(&mut self, href: &str, etag: &str) -> Fallible<()> {
self.inner.delete(href, etag)
}
}
struct CaldavStorage {
inner: DavStorage,
start_date: Option<chrono::DateTime<chrono::Utc>>, // FIXME: store as Option<(start, end)>
end_date: Option<chrono::DateTime<chrono::Utc>>,
item_types: Vec<&'static str>,
}
impl CaldavStorage {
pub fn new(
url: &str,
http_config: HttpConfig,
start_date: Option<chrono::DateTime<chrono::Utc>>,
end_date: Option<chrono::DateTime<chrono::Utc>>,
item_types: Vec<&'static str>,
) -> Self {
CaldavStorage {
inner: DavStorage::new(url, http_config),
start_date,
end_date,
item_types,
}
}
#[inline]
fn get_caldav_filters(&self) -> Vec<String> {
let mut item_types = self.item_types.clone();
let mut timefilter = "".to_owned();
if let (Some(start), Some(end)) = (self.start_date, self.end_date) {
timefilter = format!(
"<C:time-range start=\"{}\" end=\"{}\" />",
start.format(CALDAV_DT_FORMAT),
end.format(CALDAV_DT_FORMAT)
);
if item_types.is_empty() {
item_types.push("VTODO");
item_types.push("VEVENT");
}
}
item_types
.into_iter()
.map(|item_type| {
format!(
"<C:comp-filter name=\"VCALENDAR\">\
<C:comp-filter name=\"{}\">{}</C:comp-filter>\
</C:comp-filter>",
item_type, timefilter
)
})
.collect()
}
}
impl Storage for CaldavStorage {
fn list<'a>(&'a mut self) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>> {
let filters = self.get_caldav_filters();
if filters.is_empty() {
// If we don't have any filters (which is the default), taking the
// risk of sending a calendar-query is not necessary. There doesn't
// seem to be a widely-usable way to send calendar-queries with the
// same semantics as a PROPFIND request... so why not use PROPFIND
// instead?
//
// See https://github.com/dmfs/tasks/issues/118 for backstory.
self.inner.list("text/calendar")
} else {
let mut rv = vec![];
let mut headers = reqwest::header::Headers::new();
headers.set(ContentType::xml());
headers.set_raw("Depth", "1");
for filter in filters {
let data =
format!(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\
<C:calendar-query xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\
<D:prop><D:getcontenttype/><D:getetag/></D:prop>\
<C:filter>{}</C:filter>\
</C:calendar-query>", filter);
let request = self
.inner
.get_http()?
.request(report(), &self.inner.url)
.headers(headers.clone())
.body(data)
.build()?;
let response = self.inner.send_request(request)?;
rv.extend(self.inner.parse_prop_response(response, "text/calendar")?);
}
Ok(Box::new(rv.into_iter()))
}
}
fn get(&mut self, href: &str) -> Fallible<(Item, String)> {
self.inner.get(href)
}
fn upload(&mut self, item: Item) -> Fallible<(String, String)> {
let href = format!("{}.ics", generate_href(&item.get_ident()?));
self.inner.put(&href, &item, "text/calendar", None)
}
fn update(&mut self, href: &str, item: Item, etag: &str) -> Fallible<String> {
self.inner
.put(href, &item, "text/calendar", Some(etag))
.map(|x| x.1)
}
fn delete(&mut self, href: &str, etag: &str) -> Fallible<()> {
self.inner.delete(href, etag)
}
}
pub mod exports {
use super::super::http::init_http_config;
use super::*;
#[derive(Debug, Fail, Shippai)]
pub enum DavError {
#[fail(display = "Server did not return etag.")]
EtagNotFound,
}
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_init_carddav(
url: *const c_char,
username: *const c_char,
password: *const c_char,
useragent: *const c_char,
verify_cert: *const c_char,
auth_cert: *const c_char,
) -> *mut Box<Storage> {
let url = CStr::from_ptr(url);
Box::into_raw(Box::new(Box::new(CarddavStorage::new(
url.to_str().unwrap(),
init_http_config(username, password, useragent, verify_cert, auth_cert),
))))
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_init_caldav(
url: *const c_char,
username: *const c_char,
password: *const c_char,
useragent: *const c_char,
verify_cert: *const c_char,
auth_cert: *const c_char,
start_date: i64,
end_date: i64,
include_vevent: bool,
include_vjournal: bool,
include_vtodo: bool,
) -> *mut Box<Storage> {
let url = CStr::from_ptr(url);
let parse_date = |i| {
if i > 0 {
Some(chrono::DateTime::from_utc(
chrono::NaiveDateTime::from_timestamp(i, 0),
chrono::Utc,
))
} else {
None
}
};
let mut item_types = vec![];
if include_vevent {
item_types.push("VEVENT");
}
if include_vjournal {
item_types.push("VJOURNAL");
}
if include_vtodo {
item_types.push("VTODO");
}
Box::into_raw(Box::new(Box::new(CaldavStorage::new(
url.to_str().unwrap(),
init_http_config(username, password, useragent, verify_cert, auth_cert),
parse_date(start_date),
parse_date(end_date),
item_types,
))))
}
}
use exports::DavError;

View file

@ -1,110 +0,0 @@
use quick_xml;
use quick_xml::events::Event;
use errors::*;
use std::io::BufRead;
#[derive(Debug)]
pub struct Response {
pub href: Option<String>,
pub etag: Option<String>,
pub mimetype: Option<String>,
pub has_collection_tag: bool,
}
impl Response {
pub fn new() -> Self {
Response {
href: None,
etag: None,
has_collection_tag: false,
mimetype: None,
}
}
}
pub struct ListingParser<T: BufRead> {
reader: quick_xml::Reader<T>,
ns_buf: Vec<u8>,
}
impl<T: BufRead> ListingParser<T> {
pub fn new(mut reader: quick_xml::Reader<T>) -> Self {
reader.expand_empty_elements(true);
reader.trim_text(true);
reader.check_end_names(true);
reader.check_comments(false);
ListingParser {
reader,
ns_buf: vec![],
}
}
fn next_response(&mut self) -> Fallible<Option<Response>> {
let mut buf = vec![];
#[derive(Debug, Clone, Copy)]
enum State {
Outer,
Response,
Href,
ContentType,
Etag,
};
let mut state = State::Outer;
let mut current_response = Response::new();
loop {
match self
.reader
.read_namespaced_event(&mut buf, &mut self.ns_buf)?
{
(ns, Event::Start(ref e)) => {
match (state, ns, e.local_name()) {
(State::Outer, Some(b"DAV:"), b"response") => state = State::Response,
(State::Response, Some(b"DAV:"), b"href") => state = State::Href,
(State::Response, Some(b"DAV:"), b"getetag") => state = State::Etag,
(State::Response, Some(b"DAV:"), b"getcontenttype") => {
state = State::ContentType
}
(State::Response, Some(b"DAV:"), b"collection") => {
current_response.has_collection_tag = true;
}
_ => (),
}
debug!("State: {:?}", state);
}
(_, Event::Text(e)) => {
let txt = e.unescape_and_decode(&self.reader)?;
match state {
State::Href => current_response.href = Some(txt),
State::ContentType => current_response.mimetype = Some(txt),
State::Etag => current_response.etag = Some(txt),
_ => continue,
}
state = State::Response;
}
(ns, Event::End(e)) => match (state, ns, e.local_name()) {
(State::Response, Some(b"DAV:"), b"response") => {
return Ok(Some(current_response))
}
_ => (),
},
(_, Event::Eof) => return Ok(None),
_ => (),
}
}
}
pub fn get_all_responses(&mut self) -> Fallible<Vec<Response>> {
let mut rv = vec![];
while let Some(x) = self.next_response()? {
rv.push(x);
}
Ok(rv)
}
}

View file

@ -1,196 +0,0 @@
pub use super::dav::exports::*;
pub use super::filesystem::exports::*;
pub use super::http::exports::*;
pub use super::singlefile::exports::*;
use super::Storage;
use errors::*;
use item::Item;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_free(storage: *mut Box<Storage>) {
let _: Box<Box<Storage>> = Box::from_raw(storage);
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_list(
storage: *mut Box<Storage>,
err: *mut *mut ShippaiError,
) -> *mut VdirsyncerStorageListing {
if let Some(x) = export_result((**storage).list(), err) {
Box::into_raw(Box::new(VdirsyncerStorageListing {
iterator: x,
href: None,
etag: None,
}))
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_get(
storage: *mut Box<Storage>,
c_href: *const c_char,
err: *mut *mut ShippaiError,
) -> *mut VdirsyncerStorageGetResult {
let href = CStr::from_ptr(c_href);
if let Some((item, href)) = export_result((**storage).get(href.to_str().unwrap()), err) {
Box::into_raw(Box::new(VdirsyncerStorageGetResult {
item: Box::into_raw(Box::new(item)),
etag: CString::new(href).unwrap().into_raw(),
}))
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_upload(
storage: *mut Box<Storage>,
item: *mut Item,
err: *mut *mut ShippaiError,
) -> *mut VdirsyncerStorageUploadResult {
if let Some((href, etag)) = export_result((**storage).upload((*item).clone()), err) {
Box::into_raw(Box::new(VdirsyncerStorageUploadResult {
href: CString::new(href).unwrap().into_raw(),
etag: CString::new(etag).unwrap().into_raw(),
}))
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_update(
storage: *mut Box<Storage>,
c_href: *const c_char,
item: *mut Item,
c_etag: *const c_char,
err: *mut *mut ShippaiError,
) -> *const c_char {
let href = CStr::from_ptr(c_href);
let etag = CStr::from_ptr(c_etag);
let res = (**storage).update(
href.to_str().unwrap(),
(*item).clone(),
etag.to_str().unwrap(),
);
if let Some(etag) = export_result(res, err) {
CString::new(etag).unwrap().into_raw()
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_delete(
storage: *mut Box<Storage>,
c_href: *const c_char,
c_etag: *const c_char,
err: *mut *mut ShippaiError,
) {
let href = CStr::from_ptr(c_href);
let etag = CStr::from_ptr(c_etag);
let res = (**storage).delete(href.to_str().unwrap(), etag.to_str().unwrap());
let _ = export_result(res, err);
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_buffered(storage: *mut Box<Storage>) {
(**storage).buffered();
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_flush(
storage: *mut Box<Storage>,
err: *mut *mut ShippaiError,
) {
let _ = export_result((**storage).flush(), err);
}
pub struct VdirsyncerStorageListing {
iterator: Box<Iterator<Item = (String, String)>>,
href: Option<String>,
etag: Option<String>,
}
impl VdirsyncerStorageListing {
pub fn advance(&mut self) -> bool {
match self.iterator.next() {
Some((href, etag)) => {
self.href = Some(href);
self.etag = Some(etag);
true
}
None => {
self.href = None;
self.etag = None;
false
}
}
}
pub fn get_href(&mut self) -> Option<String> {
self.href.take()
}
pub fn get_etag(&mut self) -> Option<String> {
self.etag.take()
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_storage_listing(listing: *mut VdirsyncerStorageListing) {
let _: Box<VdirsyncerStorageListing> = Box::from_raw(listing);
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_advance_storage_listing(
listing: *mut VdirsyncerStorageListing,
) -> bool {
(*listing).advance()
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_listing_get_href(
listing: *mut VdirsyncerStorageListing,
) -> *const c_char {
CString::new((*listing).get_href().unwrap())
.unwrap()
.into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_storage_listing_get_etag(
listing: *mut VdirsyncerStorageListing,
) -> *const c_char {
CString::new((*listing).get_etag().unwrap())
.unwrap()
.into_raw()
}
#[repr(C)]
pub struct VdirsyncerStorageGetResult {
pub item: *mut Item,
pub etag: *const c_char,
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_storage_get_result(res: *mut VdirsyncerStorageGetResult) {
let _: Box<VdirsyncerStorageGetResult> = Box::from_raw(res);
}
#[repr(C)]
pub struct VdirsyncerStorageUploadResult {
pub href: *const c_char,
pub etag: *const c_char,
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_storage_upload_result(
res: *mut VdirsyncerStorageUploadResult,
) {
let _: Box<VdirsyncerStorageUploadResult> = Box::from_raw(res);
}

View file

@ -1,220 +0,0 @@
use super::Storage;
use errors::*;
use failure;
use libc;
use std::fs;
use std::io;
use std::io::{Read, Write};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use super::utils;
use item::Item;
use atomicwrites::{AllowOverwrite, AtomicFile, DisallowOverwrite};
pub struct FilesystemStorage {
path: PathBuf,
fileext: String,
post_hook: Option<String>,
}
impl FilesystemStorage {
pub fn new<P: AsRef<Path>>(path: P, fileext: &str, post_hook: Option<String>) -> Self {
FilesystemStorage {
path: path.as_ref().to_owned(),
fileext: fileext.into(),
post_hook,
}
}
fn get_href(&self, ident: Option<&str>) -> String {
let href_base = match ident {
Some(x) => utils::generate_href(x),
None => utils::random_href(),
};
format!("{}{}", href_base, self.fileext)
}
fn get_filepath(&self, href: &str) -> PathBuf {
self.path.join(href)
}
fn run_post_hook<S: AsRef<::std::ffi::OsStr>>(&self, fpath: S) {
if let Some(ref cmd) = self.post_hook {
let status = match Command::new(cmd).arg(fpath).status() {
Ok(x) => x,
Err(e) => {
warn!("Failed to run external hook: {}", e);
return;
}
};
if !status.success() {
if let Some(code) = status.code() {
warn!("External hook exited with error code {}.", code);
} else {
warn!("External hook was killed.");
}
}
}
}
}
#[inline]
fn handle_io_error(href: &str, e: io::Error) -> failure::Error {
match e.kind() {
io::ErrorKind::NotFound => Error::ItemNotFound {
href: href.to_owned(),
}.into(),
io::ErrorKind::AlreadyExists => Error::ItemAlreadyExisting {
href: href.to_owned(),
}.into(),
_ => e.into(),
}
}
pub mod exports {
use super::*;
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_init_filesystem(
path: *const c_char,
fileext: *const c_char,
post_hook: *const c_char,
) -> *mut Box<Storage> {
let path_c = CStr::from_ptr(path);
let fileext_c = CStr::from_ptr(fileext);
let post_hook_c = CStr::from_ptr(post_hook);
let post_hook_str = post_hook_c.to_str().unwrap();
Box::into_raw(Box::new(Box::new(FilesystemStorage::new(
path_c.to_str().unwrap(),
fileext_c.to_str().unwrap(),
if post_hook_str.is_empty() {
None
} else {
Some(post_hook_str.to_owned())
},
))))
}
}
#[inline]
fn etag_from_file(metadata: &fs::Metadata) -> String {
format!(
"{}.{};{}",
metadata.mtime(),
metadata.mtime_nsec(),
metadata.ino()
)
}
impl Storage for FilesystemStorage {
fn list<'a>(&'a mut self) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>> {
let mut rv: Vec<(String, String)> = vec![];
for entry_res in fs::read_dir(&self.path)? {
let entry = entry_res?;
let metadata = entry.metadata()?;
if !metadata.is_file() {
continue;
}
let fname: String = match entry.file_name().into_string() {
Ok(x) => x,
Err(_) => continue,
};
if !fname.ends_with(&self.fileext) {
continue;
}
rv.push((fname, etag_from_file(&metadata)));
}
Ok(Box::new(rv.into_iter()))
}
fn get(&mut self, href: &str) -> Fallible<(Item, String)> {
let fpath = self.get_filepath(href);
let mut f = match fs::File::open(fpath) {
Ok(x) => x,
Err(e) => Err(handle_io_error(href, e))?,
};
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok((Item::from_raw(s), etag_from_file(&f.metadata()?)))
}
fn upload(&mut self, item: Item) -> Fallible<(String, String)> {
#[inline]
fn inner(s: &mut FilesystemStorage, item: &Item, href: &str) -> io::Result<String> {
let filepath = s.get_filepath(href);
let af = AtomicFile::new(&filepath, DisallowOverwrite);
let content = item.get_raw();
af.write(|f| f.write_all(content.as_bytes()))?;
let new_etag = etag_from_file(&fs::metadata(&filepath)?);
s.run_post_hook(filepath);
Ok(new_etag)
}
let ident = item.get_ident()?;
let mut href = self.get_href(Some(&ident));
let etag = match inner(self, &item, &href) {
Ok(x) => x,
Err(ref e) if e.raw_os_error() == Some(libc::ENAMETOOLONG) => {
href = self.get_href(None);
match inner(self, &item, &href) {
Ok(x) => x,
Err(e) => Err(handle_io_error(&href, e))?,
}
}
Err(e) => Err(handle_io_error(&href, e))?,
};
Ok((href, etag))
}
fn update(&mut self, href: &str, item: Item, etag: &str) -> Fallible<String> {
let filepath = self.get_filepath(href);
let metadata = match fs::metadata(&filepath) {
Ok(x) => x,
Err(e) => Err(handle_io_error(href, e))?,
};
let actual_etag = etag_from_file(&metadata);
if actual_etag != etag {
Err(Error::WrongEtag {
href: href.to_owned(),
})?;
}
let af = AtomicFile::new(&filepath, AllowOverwrite);
let content = item.get_raw();
af.write(|f| f.write_all(content.as_bytes()))?;
let new_etag = etag_from_file(&fs::metadata(filepath)?);
Ok(new_etag)
}
fn delete(&mut self, href: &str, etag: &str) -> Fallible<()> {
let filepath = self.get_filepath(href);
let metadata = match fs::metadata(&filepath) {
Ok(x) => x,
Err(e) => Err(handle_io_error(href, e))?,
};
let actual_etag = etag_from_file(&metadata);
if actual_etag != etag {
Err(Error::WrongEtag {
href: href.to_owned(),
})?;
}
fs::remove_file(filepath)?;
Ok(())
}
}

View file

@ -1,230 +0,0 @@
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use std::ffi::CStr;
use std::os::raw::c_char;
use reqwest;
use super::singlefile::split_collection;
use super::Storage;
use errors::*;
use item::Item;
type ItemCache = BTreeMap<String, (Item, String)>;
pub type Username = String;
pub type Password = String;
pub type Auth = (Username, Password);
/// Wrapper around Client.execute to enable logging
#[inline]
pub fn send_request(
client: &reqwest::Client,
request: reqwest::Request,
) -> Fallible<reqwest::Response> {
debug!("> {} {}", request.method(), request.url());
for header in request.headers().iter() {
debug!("> {}: {}", header.name(), header.value_string());
}
debug!("> {:?}", request.body());
debug!("> ---");
let response = client.execute(request)?;
debug!("< {:?}", response.status());
for header in response.headers().iter() {
debug!("< {}: {}", header.name(), header.value_string());
}
Ok(response)
}
#[derive(Clone)]
pub struct HttpConfig {
pub auth: Option<Auth>,
pub useragent: Option<String>,
pub verify_cert: Option<String>,
pub auth_cert: Option<String>,
}
impl HttpConfig {
pub fn into_connection(self) -> Fallible<reqwest::ClientBuilder> {
let mut headers = reqwest::header::Headers::new();
if let Some((username, password)) = self.auth {
headers.set(reqwest::header::Authorization(reqwest::header::Basic {
username,
password: Some(password),
}));
}
if let Some(useragent) = self.useragent {
headers.set(reqwest::header::UserAgent::new(useragent));
}
let mut client = reqwest::Client::builder();
client.default_headers(headers);
if let Some(verify_cert) = self.verify_cert {
let mut buf = Vec::new();
File::open(verify_cert)?.read_to_end(&mut buf)?;
let cert = reqwest::Certificate::from_pem(&buf)?;
client.add_root_certificate(cert);
}
// TODO: auth_cert https://github.com/sfackler/rust-native-tls/issues/27
Ok(client)
}
}
pub struct HttpStorage {
url: String,
// href -> (item, etag)
items_cache: Option<ItemCache>,
http_config: HttpConfig,
}
impl HttpStorage {
pub fn new(url: String, http_config: HttpConfig) -> Self {
HttpStorage {
url,
items_cache: None,
http_config,
}
}
fn get_items(&mut self) -> Fallible<&mut ItemCache> {
if self.items_cache.is_none() {
self.list()?;
}
Ok(self.items_cache.as_mut().unwrap())
}
}
impl Storage for HttpStorage {
fn list<'a>(&'a mut self) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>> {
let client = self.http_config.clone().into_connection()?.build()?;
let mut response = handle_http_error(&self.url, client.get(&self.url).send()?)?;
let s = response.text()?;
let mut new_cache = BTreeMap::new();
for component in split_collection(&s)? {
let mut item = Item::from_component(component);
item = item.with_uid(&item.get_hash()?)?;
let ident = item.get_ident()?;
let hash = item.get_hash()?;
new_cache.insert(ident, (item, hash));
}
self.items_cache = Some(new_cache);
Ok(Box::new(self.items_cache.as_ref().unwrap().iter().map(
|(href, &(_, ref etag))| (href.clone(), etag.clone()),
)))
}
fn get(&mut self, href: &str) -> Fallible<(Item, String)> {
match self.get_items()?.get(href) {
Some(&(ref href, ref etag)) => Ok((href.clone(), etag.clone())),
None => Err(Error::ItemNotFound {
href: href.to_owned(),
})?,
}
}
fn upload(&mut self, _item: Item) -> Fallible<(String, String)> {
Err(Error::ReadOnly)?
}
fn update(&mut self, _href: &str, _item: Item, _etag: &str) -> Fallible<String> {
Err(Error::ReadOnly)?
}
fn delete(&mut self, _href: &str, _etag: &str) -> Fallible<()> {
Err(Error::ReadOnly)?
}
}
pub mod exports {
use super::*;
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_init_http(
url: *const c_char,
username: *const c_char,
password: *const c_char,
useragent: *const c_char,
verify_cert: *const c_char,
auth_cert: *const c_char,
) -> *mut Box<Storage> {
let url = CStr::from_ptr(url);
Box::into_raw(Box::new(Box::new(HttpStorage::new(
url.to_str().unwrap().to_owned(),
init_http_config(username, password, useragent, verify_cert, auth_cert),
))))
}
}
pub fn handle_http_error(href: &str, mut r: reqwest::Response) -> Fallible<reqwest::Response> {
if !r.status().is_success() {
debug!("< Error response, dumping body:");
debug!("< {:?}", r.text());
}
match r.status() {
reqwest::StatusCode::NotFound => Err(Error::ItemNotFound {
href: href.to_owned(),
})?,
reqwest::StatusCode::UnsupportedMediaType => Err(Error::UnsupportedVobject {
href: href.to_owned(),
})?,
_ => Ok(r.error_for_status()?),
}
}
pub unsafe fn init_http_config(
username: *const c_char,
password: *const c_char,
useragent: *const c_char,
verify_cert: *const c_char,
auth_cert: *const c_char,
) -> HttpConfig {
let username = CStr::from_ptr(username);
let password = CStr::from_ptr(password);
let username_dec = username.to_str().unwrap();
let password_dec = password.to_str().unwrap();
let useragent = CStr::from_ptr(useragent);
let useragent_dec = useragent.to_str().unwrap();
let verify_cert = CStr::from_ptr(verify_cert);
let verify_cert_dec = verify_cert.to_str().unwrap();
let auth_cert = CStr::from_ptr(auth_cert);
let auth_cert_dec = auth_cert.to_str().unwrap();
let auth = if !username_dec.is_empty() && !password_dec.is_empty() {
Some((username_dec.to_owned(), password_dec.to_owned()))
} else {
None
};
HttpConfig {
auth,
useragent: if useragent_dec.is_empty() {
None
} else {
Some(useragent_dec.to_owned())
},
verify_cert: if verify_cert_dec.is_empty() {
None
} else {
Some(verify_cert_dec.to_owned())
},
auth_cert: if auth_cert_dec.is_empty() {
None
} else {
Some(auth_cert_dec.to_owned())
},
}
}

View file

@ -1,54 +0,0 @@
mod dav;
pub mod exports;
mod filesystem;
mod http;
mod singlefile;
mod utils;
use errors::Fallible;
use item::Item;
type ItemAndEtag = (Item, String);
pub trait Storage {
/// returns an iterator of `(href, etag)`
fn list<'a>(&'a mut self) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>>;
///Fetch a single item.
///
///:param href: href to fetch
///:returns: (item, etag)
///:raises: :exc:`vdirsyncer.exceptions.PreconditionFailed` if item can't be found.
fn get(&mut self, href: &str) -> Fallible<ItemAndEtag>;
/// Upload a new item.
///
/// In cases where the new etag cannot be atomically determined (i.e. in the same
/// "transaction" as the upload itself), this method may return `None` as etag. This
/// special case only exists because of DAV. Avoid this situation whenever possible.
///
/// Returns `(href, etag)`
fn upload(&mut self, item: Item) -> Fallible<(String, String)>;
/// Update an item.
///
/// The etag may be none in some cases, see `upload`.
///
/// Returns `etag`
fn update(&mut self, href: &str, item: Item, etag: &str) -> Fallible<String>;
/// Delete an item by href.
fn delete(&mut self, href: &str, etag: &str) -> Fallible<()>;
/// Enter buffered mode for storages that support it.
///
/// Uploads, updates and deletions may not be effective until `flush` is explicitly called.
///
/// Use this if you will potentially write a lot of data to the storage, it improves
/// performance for storages that implement it.
fn buffered(&mut self) {}
/// Write back all changes to the collection.
fn flush(&mut self) -> Fallible<()> {
Ok(())
}
}

View file

@ -1,370 +0,0 @@
use super::Storage;
use errors::*;
use std::collections::btree_map::Entry::*;
use std::collections::{BTreeMap, BTreeSet};
use std::fs::{metadata, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use vobject;
use atomicwrites::{AllowOverwrite, AtomicFile};
use item::Item;
type ItemCache = BTreeMap<String, (Item, String)>;
pub struct SinglefileStorage {
path: PathBuf,
// href -> (item, etag)
items_cache: Option<(ItemCache, SystemTime)>,
buffered_mode: bool,
dirty_cache: bool,
}
impl SinglefileStorage {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
SinglefileStorage {
path: path.as_ref().to_owned(),
items_cache: None,
buffered_mode: false,
dirty_cache: false,
}
}
fn get_items(&mut self) -> Fallible<&mut ItemCache> {
if self.items_cache.is_none() {
self.list()?;
}
Ok(&mut self.items_cache.as_mut().unwrap().0)
}
fn write_back(&mut self) -> Fallible<()> {
self.dirty_cache = true;
if self.buffered_mode {
return Ok(());
}
self.flush()?;
Ok(())
}
}
pub mod exports {
use super::*;
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_init_singlefile(path: *const c_char) -> *mut Box<Storage> {
let cstring = CStr::from_ptr(path);
Box::into_raw(Box::new(Box::new(SinglefileStorage::new(
cstring.to_str().unwrap(),
))))
}
}
impl Storage for SinglefileStorage {
fn list<'a>(&'a mut self) -> Fallible<Box<Iterator<Item = (String, String)> + 'a>> {
let mut new_cache = BTreeMap::new();
let mtime = metadata(&self.path)?.modified()?;
let mut f = File::open(&self.path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
for component in split_collection(&s)? {
let item = Item::from_component(component);
let hash = item.get_hash()?;
let ident = item.get_ident()?;
new_cache.insert(ident, (item, hash));
}
self.items_cache = Some((new_cache, mtime));
self.dirty_cache = false;
Ok(Box::new(self.items_cache.as_ref().unwrap().0.iter().map(
|(href, &(_, ref etag))| (href.clone(), etag.clone()),
)))
}
fn get(&mut self, href: &str) -> Fallible<(Item, String)> {
match self.get_items()?.get(href) {
Some(&(ref href, ref etag)) => Ok((href.clone(), etag.clone())),
None => Err(Error::ItemNotFound {
href: href.to_owned(),
})?,
}
}
fn upload(&mut self, item: Item) -> Fallible<(String, String)> {
let hash = item.get_hash()?;
let href = item.get_ident()?;
match self.get_items()?.entry(href.clone()) {
Occupied(_) => Err(Error::ItemAlreadyExisting { href: href.clone() })?,
Vacant(vc) => vc.insert((item, hash.clone())),
};
self.write_back()?;
Ok((href, hash))
}
fn update(&mut self, href: &str, item: Item, etag: &str) -> Fallible<String> {
let hash = match self.get_items()?.entry(href.to_owned()) {
Occupied(mut oc) => {
if oc.get().1 == etag {
let hash = item.get_hash()?;
oc.insert((item, hash.clone()));
hash
} else {
Err(Error::WrongEtag {
href: href.to_owned(),
})?
}
}
Vacant(_) => Err(Error::ItemNotFound {
href: href.to_owned(),
})?,
};
self.write_back()?;
Ok(hash)
}
fn delete(&mut self, href: &str, etag: &str) -> Fallible<()> {
match self.get_items()?.entry(href.to_owned()) {
Occupied(oc) => {
if oc.get().1 == etag {
oc.remove();
} else {
Err(Error::WrongEtag {
href: href.to_owned(),
})?
}
}
Vacant(_) => Err(Error::ItemNotFound {
href: href.to_owned(),
})?,
}
self.write_back()?;
Ok(())
}
fn buffered(&mut self) {
self.buffered_mode = true;
}
fn flush(&mut self) -> Fallible<()> {
if !self.dirty_cache {
return Ok(());
}
let (items, mtime) = self.items_cache.take().unwrap();
let af = AtomicFile::new(&self.path, AllowOverwrite);
let content = join_collection(items.into_iter().map(|(_, (item, _))| item))?;
let path = &self.path;
let write_inner = |f: &mut File| -> Fallible<()> {
f.write_all(content.as_bytes())?;
let real_mtime = metadata(path)?.modified()?;
if mtime != real_mtime {
Err(Error::MtimeMismatch {
filepath: path.to_string_lossy().into_owned(),
})?;
}
Ok(())
};
af.write::<(), ::failure::Compat<::failure::Error>, _>(|f| {
write_inner(f).map_err(|e| e.compat())
})?;
self.dirty_cache = false;
Ok(())
}
}
pub fn split_collection(mut input: &str) -> Fallible<Vec<vobject::Component>> {
let mut rv = vec![];
while !input.is_empty() {
let (component, remainder) =
vobject::read_component(input).map_err(::failure::SyncFailure::new)?;
input = remainder;
match component.name.as_ref() {
"VCALENDAR" => rv.extend(split_vcalendar(component)?),
"VCARD" => rv.push(component),
"VADDRESSBOOK" => for vcard in component.subcomponents {
if vcard.name != "VCARD" {
Err(Error::UnexpectedVobject {
found: vcard.name.clone(),
expected: "VCARD".to_owned(),
})?;
}
rv.push(vcard);
},
_ => Err(Error::UnexpectedVobject {
found: component.name.clone(),
expected: "VCALENDAR | VCARD | VADDRESSBOOK".to_owned(),
})?,
}
}
Ok(rv)
}
/// Split one VCALENDAR component into multiple VCALENDAR components
#[inline]
fn split_vcalendar(mut vcalendar: vobject::Component) -> Fallible<Vec<vobject::Component>> {
vcalendar.props.remove("METHOD");
let mut timezones = BTreeMap::new(); // tzid => component
let mut subcomponents = vec![];
for component in vcalendar.subcomponents.drain(..) {
match component.name.as_ref() {
"VTIMEZONE" => {
let tzid = match component.get_only("TZID") {
Some(x) => x.value_as_string().clone(),
None => continue,
};
timezones.insert(tzid, component);
}
"VTODO" | "VEVENT" | "VJOURNAL" => subcomponents.push(component),
_ => Err(Error::UnexpectedVobject {
found: component.name.clone(),
expected: "VTIMEZONE | VTODO | VEVENT | VJOURNAL".to_owned(),
})?,
};
}
let mut by_uid = BTreeMap::new();
let mut no_uid = vec![];
for component in subcomponents {
let uid = component.get_only("UID").cloned();
let mut wrapper = match uid
.as_ref()
.and_then(|u| by_uid.remove(&u.value_as_string()))
{
Some(x) => x,
None => vcalendar.clone(),
};
let mut required_tzids = BTreeSet::new();
for props in component.props.values() {
for prop in props {
if let Some(x) = prop.params.get("TZID") {
required_tzids.insert(x.to_owned());
}
}
}
for tzid in required_tzids {
if let Some(tz) = timezones.get(&tzid) {
wrapper.subcomponents.push(tz.clone());
}
}
wrapper.subcomponents.push(component);
match uid {
Some(p) => {
by_uid.insert(p.value_as_string(), wrapper);
}
None => no_uid.push(wrapper),
}
}
Ok(by_uid
.into_iter()
.map(|(_, v)| v)
.chain(no_uid.into_iter())
.collect())
}
fn join_collection<I: Iterator<Item = Item>>(item_iter: I) -> Fallible<String> {
let mut items = item_iter.peekable();
let item_name = match items.peek() {
Some(x) => x.get_component()?.name.clone(),
None => return Ok("".to_owned()),
};
let wrapper_name = match item_name.as_ref() {
"VCARD" => "VADDRESSBOOK",
"VCALENDAR" => "VCALENDAR",
_ => Err(Error::UnexpectedVobject {
found: item_name.clone(),
expected: "VCARD | VCALENDAR".to_owned(),
})?,
};
let mut wrapper = vobject::Component::new(wrapper_name);
let mut version: Option<vobject::Property> = None;
for item in items {
let mut c = item.into_component()?;
if c.name != item_name {
return Err(Error::UnexpectedVobject {
found: c.name,
expected: item_name.clone(),
}.into());
}
if item_name == wrapper_name {
wrapper.subcomponents.extend(c.subcomponents.drain(..));
match (version.as_ref(), c.get_only("VERSION")) {
(Some(x), Some(y)) if x.raw_value != y.raw_value => {
return Err(Error::UnexpectedVobjectVersion {
expected: x.raw_value.clone(),
found: y.raw_value.clone(),
}.into());
}
(None, Some(_)) => (),
_ => continue,
}
version = c.get_only("VERSION").cloned();
} else {
wrapper.subcomponents.push(c);
}
}
if let Some(v) = version {
wrapper.set(v);
}
Ok(vobject::write_component(&wrapper))
}
#[cfg(test)]
mod tests {
use super::*;
fn check_roundtrip(raw: &str) {
let components = split_collection(raw).unwrap();
let raw2 = join_collection(components.into_iter().map(Item::from_component)).unwrap();
assert_eq!(
Item::from_raw(raw.to_owned()).get_hash().unwrap(),
Item::from_raw(raw2.to_owned()).get_hash().unwrap()
);
}
#[test]
fn test_wrapper_properties_roundtrip() {
let raw = r#"BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
X-WR-CALNAME:markus.unterwaditzer@runtastic.com
X-WR-TIMEZONE:Europe/Vienna
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
DTSTART;TZID=Europe/Vienna:20171012T153000
DTEND;TZID=Europe/Vienna:20171012T170000
DTSTAMP:20171009T085029Z
UID:test@test.com
STATUS:CONFIRMED
SUMMARY:Test
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR"#;
check_roundtrip(raw);
}
}

View file

@ -1,24 +0,0 @@
use uuid::Uuid;
fn is_href_safe(ident: &str) -> bool {
for c in ident.chars() {
match c {
'_' | '.' | '-' | '+' => (),
_ if c.is_alphanumeric() => (),
_ => return false,
}
}
true
}
pub fn generate_href(ident: &str) -> String {
if is_href_safe(ident) {
ident.to_owned()
} else {
random_href()
}
}
pub fn random_href() -> String {
format!("{}", Uuid::new_v4())
}

View file

@ -1,146 +0,0 @@
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Box_Storage Box_Storage;
typedef struct Item Item;
typedef struct ShippaiError ShippaiError;
typedef struct VdirsyncerStorageListing VdirsyncerStorageListing;
typedef struct {
Item *item;
const char *etag;
} VdirsyncerStorageGetResult;
typedef struct {
const char *href;
const char *etag;
} VdirsyncerStorageUploadResult;
extern const uint8_t SHIPPAI_VARIANT_DavError_EtagNotFound;
extern const uint8_t SHIPPAI_VARIANT_Error_ItemAlreadyExisting;
extern const uint8_t SHIPPAI_VARIANT_Error_ItemNotFound;
extern const uint8_t SHIPPAI_VARIANT_Error_ItemUnparseable;
extern const uint8_t SHIPPAI_VARIANT_Error_MtimeMismatch;
extern const uint8_t SHIPPAI_VARIANT_Error_ReadOnly;
extern const uint8_t SHIPPAI_VARIANT_Error_UnexpectedVobject;
extern const uint8_t SHIPPAI_VARIANT_Error_UnexpectedVobjectVersion;
extern const uint8_t SHIPPAI_VARIANT_Error_UnsupportedVobject;
extern const uint8_t SHIPPAI_VARIANT_Error_WrongEtag;
void shippai_free_failure(ShippaiError *t);
void shippai_free_str(char *t);
const char *shippai_get_debug(ShippaiError *t);
const char *shippai_get_display(ShippaiError *t);
uint8_t shippai_get_variant_DavError(ShippaiError *t);
uint8_t shippai_get_variant_Error(ShippaiError *t);
bool shippai_is_error_DavError(ShippaiError *t);
bool shippai_is_error_Error(ShippaiError *t);
bool vdirsyncer_advance_storage_listing(VdirsyncerStorageListing *listing);
void vdirsyncer_free_item(Item *c);
void vdirsyncer_free_storage_get_result(VdirsyncerStorageGetResult *res);
void vdirsyncer_free_storage_listing(VdirsyncerStorageListing *listing);
void vdirsyncer_free_storage_upload_result(VdirsyncerStorageUploadResult *res);
void vdirsyncer_free_str(const char *s);
const char *vdirsyncer_get_hash(Item *c, ShippaiError **err);
const char *vdirsyncer_get_raw(Item *c);
const char *vdirsyncer_get_uid(Item *c);
Box_Storage *vdirsyncer_init_caldav(const char *url,
const char *username,
const char *password,
const char *useragent,
const char *verify_cert,
const char *auth_cert,
int64_t start_date,
int64_t end_date,
bool include_vevent,
bool include_vjournal,
bool include_vtodo);
Box_Storage *vdirsyncer_init_carddav(const char *url,
const char *username,
const char *password,
const char *useragent,
const char *verify_cert,
const char *auth_cert);
Box_Storage *vdirsyncer_init_filesystem(const char *path,
const char *fileext,
const char *post_hook);
Box_Storage *vdirsyncer_init_http(const char *url,
const char *username,
const char *password,
const char *useragent,
const char *verify_cert,
const char *auth_cert);
void vdirsyncer_init_logger(void);
Box_Storage *vdirsyncer_init_singlefile(const char *path);
Item *vdirsyncer_item_from_raw(const char *s);
bool vdirsyncer_item_is_parseable(Item *c);
void vdirsyncer_storage_buffered(Box_Storage *storage);
void vdirsyncer_storage_delete(Box_Storage *storage,
const char *c_href,
const char *c_etag,
ShippaiError **err);
void vdirsyncer_storage_flush(Box_Storage *storage, ShippaiError **err);
void vdirsyncer_storage_free(Box_Storage *storage);
VdirsyncerStorageGetResult *vdirsyncer_storage_get(Box_Storage *storage,
const char *c_href,
ShippaiError **err);
VdirsyncerStorageListing *vdirsyncer_storage_list(Box_Storage *storage, ShippaiError **err);
const char *vdirsyncer_storage_listing_get_etag(VdirsyncerStorageListing *listing);
const char *vdirsyncer_storage_listing_get_href(VdirsyncerStorageListing *listing);
const char *vdirsyncer_storage_update(Box_Storage *storage,
const char *c_href,
Item *item,
const char *c_etag,
ShippaiError **err);
VdirsyncerStorageUploadResult *vdirsyncer_storage_upload(Box_Storage *storage,
Item *item,
ShippaiError **err);
Item *vdirsyncer_with_uid(Item *c, const char *uid, ShippaiError **err);

View file

@ -0,0 +1,49 @@
#!/bin/bash
#
# This script is mean to be run inside a dedicated container,
# and not interatively.
set -ex
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y build-essential fakeroot debhelper git
apt-get install -y python3-all python3-pip python3-venv
apt-get install -y ruby ruby-dev
pip3 install virtualenv virtualenv-tools3
virtualenv -p python3 /vdirsyncer/env/
gem install fpm
# See https://github.com/jordansissel/fpm/issues/1106#issuecomment-461678970
pip3 uninstall -y virtualenv
echo 'python3 -m venv "$@"' > /usr/local/bin/virtualenv
chmod +x /usr/local/bin/virtualenv
cp -r /source/ /vdirsyncer/vdirsyncer/
cd /vdirsyncer/vdirsyncer/ || exit 2
mkdir /vdirsyncer/pkgs/
basename -- *.tar.gz .tar.gz | cut -d'-' -f2 | sed -e 's/\.dev/~/g' | tee version
# XXX: Do I really not want google support included?
(echo -n *.tar.gz; echo '[google]') | tee requirements.txt
fpm --verbose \
--input-type virtualenv \
--output-type deb \
--name "vdirsyncer-latest" \
--version "$(cat version)" \
--prefix /opt/venvs/vdirsyncer-latest \
--depends python3 \
requirements.txt
mv /vdirsyncer/vdirsyncer/*.deb /vdirsyncer/pkgs/
cd /vdirsyncer/pkgs/
dpkg -i -- *.deb
# Check that it works:
LC_ALL=C.UTF-8 LANG=C.UTF-8 /opt/venvs/vdirsyncer-latest/bin/vdirsyncer --version
cp -- *.deb /source/

View file

@ -1,11 +0,0 @@
echo "export PATH=$HOME/.cargo/bin/:$HOME/.local/bin/:$PATH" >> $BASH_ENV
. $BASH_ENV
make install-rust
sudo apt-get install -y cmake
pip install --user virtualenv
virtualenv ~/env
echo ". ~/env/bin/activate" >> $BASH_ENV
. $BASH_ENV

View file

@ -1,40 +0,0 @@
ARG distro
ARG distrover
FROM $distro:$distrover
ARG distro
ARG distrover
RUN apt-get update
RUN apt-get install -y build-essential fakeroot debhelper git
RUN apt-get install -y python3-all python3-dev python3-pip
RUN apt-get install -y ruby ruby-dev
RUN apt-get install -y python-all python-pip
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
RUN apt-get install -y libssl-dev libffi-dev
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN gem install fpm
RUN pip2 install virtualenv-tools
RUN pip3 install virtualenv
RUN virtualenv -p python3 /vdirsyncer/env/
COPY . /vdirsyncer/vdirsyncer/
WORKDIR /vdirsyncer/vdirsyncer/
RUN mkdir /vdirsyncer/pkgs/
RUN basename *.tar.gz .tar.gz | cut -d'-' -f2 | sed -e 's/\.dev/~/g' | tee version
RUN (echo -n *.tar.gz; echo '[google]') | tee requirements.txt
RUN . /vdirsyncer/env/bin/activate; fpm --verbose -s virtualenv -t deb \
-n "vdirsyncer-latest" \
-v "$(cat version)" \
--prefix /opt/venvs/vdirsyncer-latest \
requirements.txt
RUN mv /vdirsyncer/vdirsyncer/*.deb /vdirsyncer/pkgs/
WORKDIR /vdirsyncer/pkgs/
RUN dpkg -i *.deb
RUN LC_ALL=C.UTF-8 LANG=C.UTF-8 /opt/venvs/vdirsyncer-latest/bin/vdirsyncer --version

View file

@ -1,19 +1,56 @@
#!/bin/sh
set -xe
distro=$1
distrover=$2
name=vdirsyncer-$distro-$distrover:latest
context="$(mktemp -d)"
python setup.py sdist -d "$context"
cp scripts/dpkg.Dockerfile "$context/Dockerfile"
set -xeu
docker build \
--build-arg distro=$distro \
--build-arg distrover=$distrover \
-t $name \
"$context"
SCRIPT_PATH=$(realpath "$0")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
docker run $name tar -c -C /vdirsyncer pkgs | tar x -C "$context"
package_cloud push pimutils/vdirsyncer/$distro/$distrover $context/pkgs/*.deb
rm -rf "$context"
# E.g.: debian, ubuntu
DISTRO=${DISTRO:1}
# E.g.: bullseye, bookwork
DISTROVER=${DISTROVER:2}
CONTAINER_NAME="vdirsyncer-${DISTRO}-${DISTROVER}"
CONTEXT="$(mktemp -d)"
DEST_DIR="$SCRIPT_DIR/../$DISTRO-$DISTROVER"
cleanup() {
rm -rf "$CONTEXT"
}
trap cleanup EXIT
# Prepare files.
cp scripts/_build_deb_in_container.bash "$CONTEXT"
python setup.py sdist -d "$CONTEXT"
docker run -it \
--name "$CONTAINER_NAME" \
--volume "$CONTEXT:/source" \
"$DISTRO:$DISTROVER" \
bash /source/_build_deb_in_container.bash
# Keep around the package filename.
PACKAGE=$(ls "$CONTEXT"/*.deb)
PACKAGE=$(basename "$PACKAGE")
# Save the build deb files.
mkdir -p "$DEST_DIR"
cp "$CONTEXT"/*.deb "$DEST_DIR"
echo Build complete! 🤖
# Packagecloud uses some internal IDs for each distro.
# Extract the one for the distro we're publishing.
DISTRO_ID=$(
curl -s \
https://"$PACKAGECLOUD_TOKEN":@packagecloud.io/api/v1/distributions.json | \
jq '.deb | .[] | select(.index_name=="'"$DISTRO"'") | .versions | .[] | select(.index_name=="'"$DISTROVER"'") | .id'
)
# Actually push the package.
curl \
-F "package[distro_version_id]=$DISTRO_ID" \
-F "package[package_file]=@$DEST_DIR/$PACKAGE" \
https://"$PACKAGECLOUD_TOKEN":@packagecloud.io/api/v1/repos/pimutils/vdirsyncer/packages.json
echo Done! ✨

View file

@ -1,11 +0,0 @@
[tool:pytest]
norecursedirs = tests/storage/servers/*
addopts = --tb=short --duration 3
[flake8]
# E731: Use a def instead of lambda expr
# E743: Ambiguous function definition
ignore = E731, E743
select = C,E,F,W,B,B9
exclude = .eggs/, tests/storage/servers/nextcloud/, build/, vdirsyncer/_native*
application-package-names = tests,vdirsyncer

137
setup.py
View file

@ -1,137 +0,0 @@
# -*- coding: utf-8 -*-
'''
Vdirsyncer synchronizes calendars and contacts.
Please refer to https://vdirsyncer.pimutils.org/en/stable/packaging.html for
how to package vdirsyncer.
'''
import os
from setuptools import Command, find_packages, setup
milksnake = 'milksnake'
requirements = [
# https://github.com/mitsuhiko/click/issues/200
'click>=5.0',
'click-log>=0.3.0, <0.4.0',
# https://github.com/pimutils/vdirsyncer/issues/478
'click-threading>=0.2',
# !=2.9.0: https://github.com/kennethreitz/requests/issues/2930
#
# >=2.4.1: https://github.com/shazow/urllib3/pull/444
# Without the above pull request, `verify=False` also disables fingerprint
# validation. This is *not* what we want, and it's not possible to
# replicate vdirsyncer's current behavior (verifying fingerprints without
# verifying against CAs) with older versions of urllib3.
'requests >=2.4.1, !=2.9.0',
# https://github.com/sigmavirus24/requests-toolbelt/pull/28
# And https://github.com/sigmavirus24/requests-toolbelt/issues/54
'requests_toolbelt >=0.4.0',
# https://github.com/untitaker/python-atomicwrites/commit/4d12f23227b6a944ab1d99c507a69fdbc7c9ed6d # noqa
'atomicwrites>=0.1.7',
milksnake,
'shippai >= 0.2.3',
]
def build_native(spec):
cmd = ['cargo', 'build']
if os.environ.get('RUST_BACKTRACE', 'false') in ('true', '1', 'full'):
dylib_folder = 'target/debug'
else:
dylib_folder = 'target/release'
cmd.append('--release')
build = spec.add_external_build(cmd=cmd, path='./rust/')
spec.add_cffi_module(
module_path='vdirsyncer._native',
dylib=lambda: build.find_dylib('vdirsyncer_rustext',
in_path=dylib_folder),
header_filename='rust/vdirsyncer_rustext.h',
# Rust bug: If thread-local storage is used, this flag is necessary
# (mitsuhiko)
rtld_flags=['NOW', 'NODELETE']
)
class PrintRequirements(Command):
description = 'Prints minimal requirements'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
for requirement in requirements:
print(requirement.replace(">", "=").replace(" ", ""))
with open('README.rst') as f:
long_description = f.read()
setup(
# General metadata
name='vdirsyncer',
author='Markus Unterwaditzer',
author_email='markus@unterwaditzer.net',
url='https://github.com/pimutils/vdirsyncer',
description='Synchronize calendars and contacts',
license='BSD',
long_description=long_description,
# Runtime dependencies
install_requires=requirements,
# Optional dependencies
extras_require={
'google': ['requests-oauthlib'],
'etesync': ['etesync']
},
# Build dependencies
setup_requires=[
'setuptools_scm != 1.12.0',
milksnake,
],
# Other
packages=find_packages(exclude=['tests.*', 'tests']),
include_package_data=True,
cmdclass={
'minimal_requirements': PrintRequirements
},
use_scm_version={
'write_to': 'vdirsyncer/version.py'
},
entry_points={
'console_scripts': ['vdirsyncer = vdirsyncer.cli:main']
},
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'License :: OSI Approved :: BSD License',
'Operating System :: POSIX',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet',
'Topic :: Utilities',
],
milksnake_tasks=[build_native],
zip_safe=False,
platforms='any'
)

View file

@ -1,4 +0,0 @@
hypothesis>=3.1
pytest
pytest-localserver
pytest-subtesthack

View file

@ -1,29 +1,26 @@
# -*- coding: utf-8 -*-
'''
"""
Test suite for vdirsyncer.
'''
"""
import random
from __future__ import annotations
import hypothesis.strategies as st
from vdirsyncer.vobject import Item
import urllib3
import urllib3.exceptions
from vdirsyncer.vobject import normalize_item
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def blow_up(*a, **kw):
raise AssertionError('Did not expect to be called.')
raise AssertionError("Did not expect to be called.")
def assert_item_equals(a, b):
assert a.hash == b.hash
assert normalize_item(a) == normalize_item(b)
VCARD_TEMPLATE = u'''BEGIN:VCARD
VCARD_TEMPLATE = """BEGIN:VCARD
VERSION:3.0
FN:Cyrus Daboo
N:Daboo;Cyrus;;;
@ -37,9 +34,9 @@ TEL;TYPE=FAX:412 605 0705
URL;VALUE=URI:http://www.example.com
X-SOMETHING:{r}
UID:{uid}
END:VCARD'''
END:VCARD"""
TASK_TEMPLATE = u'''BEGIN:VCALENDAR
TASK_TEMPLATE = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTODO
@ -51,26 +48,30 @@ SUMMARY:Book: Kowlani - Tödlicher Staub
X-SOMETHING:{r}
UID:{uid}
END:VTODO
END:VCALENDAR'''
END:VCALENDAR"""
BARE_EVENT_TEMPLATE = u'''BEGIN:VEVENT
BARE_EVENT_TEMPLATE = """BEGIN:VEVENT
DTSTART:19970714T170000Z
DTEND:19970715T035959Z
DTSTAMP:19970610T172345Z
SUMMARY:Bastille Day Party
X-SOMETHING:{r}
UID:{uid}
END:VEVENT'''
END:VEVENT"""
EVENT_TEMPLATE = u'''BEGIN:VCALENDAR
EVENT_TEMPLATE = (
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
''' + BARE_EVENT_TEMPLATE + u'''
END:VCALENDAR'''
"""
+ BARE_EVENT_TEMPLATE
+ """
END:VCALENDAR"""
)
EVENT_WITH_TIMEZONE_TEMPLATE = '''BEGIN:VCALENDAR
EVENT_WITH_TIMEZONE_TEMPLATE = (
"""BEGIN:VCALENDAR
BEGIN:VTIMEZONE
TZID:Europe/Rome
X-LIC-LOCATION:Europe/Rome
@ -89,33 +90,21 @@ DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
''' + BARE_EVENT_TEMPLATE + '''
END:VCALENDAR'''
"""
+ BARE_EVENT_TEMPLATE
+ """
END:VCALENDAR"""
)
SIMPLE_TEMPLATE = u'''BEGIN:FOO
SIMPLE_TEMPLATE = """BEGIN:FOO
UID:{uid}
X-SOMETHING:{r}
HAHA:YES
END:FOO'''
END:FOO"""
printable_characters_strategy = st.text(
st.characters(blacklist_categories=(
'Cc', 'Cs'
))
)
printable_characters_strategy = st.text(st.characters(exclude_categories=("Cc", "Cs")))
uid_strategy = st.text(
st.characters(blacklist_categories=(
'Zs', 'Zl', 'Zp',
'Cc', 'Cs'
)),
min_size=1
st.characters(exclude_categories=("Zs", "Zl", "Zp", "Cc", "Cs")), min_size=1
).filter(lambda x: x.strip() == x)
def format_item(uid=None, item_template=VCARD_TEMPLATE):
# assert that special chars are handled correctly.
r = random.random()
uid = uid or r
return Item(item_template.format(r=r, uid=uid))

View file

@ -1,44 +1,70 @@
# -*- coding: utf-8 -*-
'''
"""
General-purpose fixtures for vdirsyncer's testsuite.
'''
"""
from __future__ import annotations
import logging
import os
import aiohttp
import click_log
from hypothesis import HealthCheck, Verbosity, settings
import pytest
import pytest_asyncio
from hypothesis import HealthCheck
from hypothesis import Verbosity
from hypothesis import settings
@pytest.fixture(autouse=True)
def setup_logging():
click_log.basic_config('vdirsyncer').setLevel(logging.DEBUG)
click_log.basic_config("vdirsyncer").setLevel(logging.DEBUG)
try:
import pytest_benchmark
except ImportError:
@pytest.fixture
def benchmark():
return lambda x: x()
else:
del pytest_benchmark
settings.suppress_health_check = [HealthCheck.too_slow]
settings.register_profile(
"ci",
settings(
max_examples=1000,
verbosity=Verbosity.verbose,
suppress_health_check=[HealthCheck.too_slow],
),
)
settings.register_profile(
"deterministic",
settings(
derandomize=True,
suppress_health_check=list(HealthCheck),
),
)
settings.register_profile("dev", settings(suppress_health_check=[HealthCheck.too_slow]))
settings.register_profile("ci", settings(
max_examples=1000,
verbosity=Verbosity.verbose,
))
settings.register_profile("deterministic", settings(
derandomize=True,
perform_health_check=False
))
if os.environ.get('DETERMINISTIC_TESTS', 'false').lower() == 'true':
if os.environ.get("DETERMINISTIC_TESTS", "false").lower() == "true":
settings.load_profile("deterministic")
elif os.environ.get('CI', 'false').lower() == 'true':
elif os.environ.get("CI", "false").lower() == "true":
settings.load_profile("ci")
else:
settings.load_profile("dev")
@pytest_asyncio.fixture
async def aio_session():
async with aiohttp.ClientSession() as session:
yield session
@pytest_asyncio.fixture
async def aio_connector():
async with aiohttp.TCPConnector(limit_per_host=16) as conn:
yield conn

View file

@ -1,280 +1,390 @@
# -*- coding: utf-8 -*-
import uuid
from __future__ import annotations
import random
import textwrap
import uuid
from urllib.parse import quote as urlquote
from urllib.parse import unquote as urlunquote
import aiostream
import pytest
import pytest_asyncio
from tests import EVENT_TEMPLATE
from tests import TASK_TEMPLATE
from tests import VCARD_TEMPLATE
from tests import assert_item_equals
from tests import normalize_item
from vdirsyncer import exceptions
from vdirsyncer.storage.base import normalize_meta_value
from vdirsyncer.vobject import Item
from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
assert_item_equals, format_item
def get_server_mixin(server_name):
from . import __name__ as base
x = __import__('{}.servers.{}'.format(base, server_name), fromlist=[''])
x = __import__(f"{base}.servers.{server_name}", fromlist=[""])
return x.ServerMixin
class StorageTests(object):
def format_item(item_template, uid=None):
# assert that special chars are handled correctly.
r = random.random()
return Item(item_template.format(r=r, uid=uid or r))
class StorageTests:
storage_class = None
supports_collections = True
supports_metadata = True
@pytest.fixture(params=['VEVENT', 'VTODO', 'VCARD'])
@pytest.fixture(params=["VEVENT", "VTODO", "VCARD"])
def item_type(self, request):
'''Parametrize with all supported item types.'''
"""Parametrize with all supported item types."""
return request.param
@pytest.fixture
def get_storage_args(self):
'''
"""
Return a function with the following properties:
:param collection: The name of the collection to create and use.
'''
raise NotImplementedError()
"""
raise NotImplementedError
@pytest.fixture
def s(self, get_storage_args):
return self.storage_class(**get_storage_args())
@pytest_asyncio.fixture
async def s(self, get_storage_args):
rv = self.storage_class(**await get_storage_args())
return rv
@pytest.fixture
def get_item(self, item_type):
template = {
'VEVENT': EVENT_TEMPLATE,
'VTODO': TASK_TEMPLATE,
'VCARD': VCARD_TEMPLATE,
"VEVENT": EVENT_TEMPLATE,
"VTODO": TASK_TEMPLATE,
"VCARD": VCARD_TEMPLATE,
}[item_type]
return lambda **kw: format_item(item_template=template, **kw)
return lambda **kw: format_item(template, **kw)
@pytest.fixture
def requires_collections(self):
if not self.supports_collections:
pytest.skip('This storage does not support collections.')
pytest.skip("This storage does not support collections.")
@pytest.fixture
def requires_metadata(self):
if not self.supports_metadata:
pytest.skip('This storage does not support metadata.')
pytest.skip("This storage does not support metadata.")
def test_generic(self, s, get_item):
@pytest.mark.asyncio
async def test_generic(self, s, get_item):
items = [get_item() for i in range(1, 10)]
hrefs = []
for item in items:
href, etag = s.upload(item)
href, etag = await s.upload(item)
if etag is None:
_, etag = s.get(href)
_, etag = await s.get(href)
hrefs.append((href, etag))
hrefs.sort()
assert hrefs == sorted(s.list())
assert hrefs == sorted(await aiostream.stream.list(s.list()))
for href, etag in hrefs:
assert isinstance(href, (str, bytes))
assert isinstance(etag, (str, bytes))
assert s.has(href)
item, etag2 = s.get(href)
assert await s.has(href)
item, etag2 = await s.get(href)
assert etag == etag2
def test_empty_get_multi(self, s):
assert list(s.get_multi([])) == []
@pytest.mark.asyncio
async def test_empty_get_multi(self, s):
assert await aiostream.stream.list(s.get_multi([])) == []
def test_get_multi_duplicates(self, s, get_item):
href, etag = s.upload(get_item())
@pytest.mark.asyncio
async def test_get_multi_duplicates(self, s, get_item):
href, etag = await s.upload(get_item())
if etag is None:
_, etag = s.get(href)
(href2, item, etag2), = s.get_multi([href] * 2)
_, etag = await s.get(href)
((href2, _item, etag2),) = await aiostream.stream.list(s.get_multi([href] * 2))
assert href2 == href
assert etag2 == etag
def test_upload_already_existing(self, s, get_item):
@pytest.mark.asyncio
async def test_upload_already_existing(self, s, get_item):
item = get_item()
s.upload(item)
await s.upload(item)
with pytest.raises(exceptions.PreconditionFailed):
s.upload(item)
await s.upload(item)
def test_upload(self, s, get_item):
@pytest.mark.asyncio
async def test_upload(self, s, get_item):
item = get_item()
href, etag = s.upload(item)
assert_item_equals(s.get(href)[0], item)
href, _etag = await s.upload(item)
assert_item_equals((await s.get(href))[0], item)
def test_update(self, s, get_item):
@pytest.mark.asyncio
async def test_update(self, s, get_item):
item = get_item()
href, etag = s.upload(item)
href, etag = await s.upload(item)
if etag is None:
_, etag = s.get(href)
assert_item_equals(s.get(href)[0], item)
_, etag = await s.get(href)
assert_item_equals((await s.get(href))[0], item)
new_item = get_item(uid=item.uid)
new_etag = s.update(href, new_item, etag)
new_etag = await s.update(href, new_item, etag)
if new_etag is None:
_, new_etag = s.get(href)
_, new_etag = await s.get(href)
# See https://github.com/pimutils/vdirsyncer/issues/48
assert isinstance(new_etag, (bytes, str))
assert_item_equals(s.get(href)[0], new_item)
assert_item_equals((await s.get(href))[0], new_item)
def test_update_nonexisting(self, s, get_item):
@pytest.mark.asyncio
async def test_update_nonexisting(self, s, get_item):
item = get_item()
with pytest.raises(exceptions.PreconditionFailed):
s.update('huehue', item, '"123"')
await s.update("huehue", item, '"123"')
def test_wrong_etag(self, s, get_item):
@pytest.mark.asyncio
async def test_wrong_etag(self, s, get_item):
item = get_item()
href, etag = s.upload(item)
href, _etag = await s.upload(item)
with pytest.raises(exceptions.PreconditionFailed):
s.update(href, item, '"lolnope"')
await s.update(href, item, '"lolnope"')
with pytest.raises(exceptions.PreconditionFailed):
s.delete(href, '"lolnope"')
await s.delete(href, '"lolnope"')
def test_delete(self, s, get_item):
href, etag = s.upload(get_item())
if etag is None:
_, etag = s.get(href)
s.delete(href, etag)
assert not list(s.list())
@pytest.mark.asyncio
async def test_delete(self, s, get_item):
href, etag = await s.upload(get_item())
await s.delete(href, etag)
assert not await aiostream.stream.list(s.list())
def test_delete_nonexisting(self, s, get_item):
@pytest.mark.asyncio
async def test_delete_nonexisting(self, s, get_item):
with pytest.raises(exceptions.PreconditionFailed):
s.delete('1', '"123"')
await s.delete("1", '"123"')
def test_list(self, s, get_item):
assert not list(s.list())
href, etag = s.upload(get_item())
@pytest.mark.asyncio
async def test_list(self, s, get_item):
assert not await aiostream.stream.list(s.list())
href, etag = await s.upload(get_item())
if etag is None:
_, etag = s.get(href)
assert list(s.list()) == [(href, etag)]
_, etag = await s.get(href)
assert await aiostream.stream.list(s.list()) == [(href, etag)]
def test_has(self, s, get_item):
assert not s.has('asd')
href, etag = s.upload(get_item())
if etag is None:
_, etag = s.get(href)
assert s.has(href)
assert not s.has('asd')
s.delete(href, etag)
assert not s.has(href)
@pytest.mark.asyncio
async def test_has(self, s, get_item):
assert not await s.has("asd")
href, etag = await s.upload(get_item())
assert await s.has(href)
assert not await s.has("asd")
await s.delete(href, etag)
assert not await s.has(href)
def test_update_others_stay_the_same(self, s, get_item):
@pytest.mark.asyncio
async def test_update_others_stay_the_same(self, s, get_item):
info = {}
for _ in range(4):
href, etag = s.upload(get_item())
href, etag = await s.upload(get_item())
if etag is None:
_, etag = s.get(href)
_, etag = await s.get(href)
info[href] = etag
assert dict(
(href, etag) for href, item, etag
in s.get_multi(href for href, etag in info.items())
) == info
items = await aiostream.stream.list(
s.get_multi(href for href, etag in info.items())
)
assert {href: etag for href, item, etag in items} == info
def test_repr(self, s, get_storage_args):
def test_repr(self, s):
assert self.storage_class.__name__ in repr(s)
assert s.instance_name is None
def test_discover(self, requires_collections, get_storage_args, get_item):
@pytest.mark.asyncio
async def test_discover(
self,
requires_collections,
get_storage_args,
get_item,
aio_connector,
):
collections = set()
for i in range(1, 5):
collection = 'test{}'.format(i)
s = self.storage_class(**get_storage_args(collection=collection))
assert not list(s.list())
s.upload(get_item())
collection = f"test{i}"
s = self.storage_class(**await get_storage_args(collection=collection))
assert not await aiostream.stream.list(s.list())
await s.upload(get_item())
collections.add(s.collection)
actual = set(
c['collection'] for c in
self.storage_class.discover(**get_storage_args(collection=None))
discovered = await aiostream.stream.list(
self.storage_class.discover(**await get_storage_args(collection=None))
)
actual = {c["collection"] for c in discovered}
assert actual >= collections
def test_create_collection(self, requires_collections, get_storage_args,
get_item):
if getattr(self, 'dav_server', '') in \
('icloud', 'fastmail', 'davical'):
pytest.skip('Manual cleanup would be necessary.')
@pytest.mark.asyncio
async def test_create_collection(
self,
requires_collections,
get_storage_args,
get_item,
):
if getattr(self, "dav_server", "") in ("icloud", "fastmail", "davical"):
pytest.skip("Manual cleanup would be necessary.")
if getattr(self, "dav_server", "") == "radicale":
pytest.skip("Radicale does not support collection creation")
args = get_storage_args(collection=None)
args['collection'] = 'test'
args = await get_storage_args(collection=None)
args["collection"] = "test"
s = self.storage_class(
**self.storage_class.create_collection(**args)
s = self.storage_class(**await self.storage_class.create_collection(**args))
href = (await s.upload(get_item()))[0]
assert href in await aiostream.stream.list(
(href async for href, etag in s.list())
)
href = s.upload(get_item())[0]
assert href in set(href for href, etag in s.list())
def test_discover_collection_arg(self, requires_collections,
get_storage_args):
args = get_storage_args(collection='test2')
@pytest.mark.asyncio
async def test_discover_collection_arg(
self, requires_collections, get_storage_args
):
args = await get_storage_args(collection="test2")
with pytest.raises(TypeError) as excinfo:
list(self.storage_class.discover(**args))
await aiostream.stream.list(self.storage_class.discover(**args))
assert 'collection argument must not be given' in str(excinfo.value)
def test_collection_arg(self, get_storage_args):
if self.storage_class.storage_name.startswith('etesync'):
pytest.skip('etesync uses UUIDs.')
assert "collection argument must not be given" in str(excinfo.value)
@pytest.mark.asyncio
async def test_collection_arg(self, get_storage_args):
if self.supports_collections:
s = self.storage_class(**get_storage_args(collection='test2'))
s = self.storage_class(**await get_storage_args(collection="test2"))
# Can't do stronger assertion because of radicale, which needs a
# fileextension to guess the collection type.
assert 'test2' in s.collection
assert "test2" in s.collection
else:
with pytest.raises(ValueError):
self.storage_class(collection='ayy', **get_storage_args())
self.storage_class(collection="ayy", **await get_storage_args())
def test_case_sensitive_uids(self, s, get_item):
if s.storage_name == 'filesystem':
pytest.skip('Behavior depends on the filesystem.')
@pytest.mark.asyncio
async def test_case_sensitive_uids(self, s, get_item):
if s.storage_name == "filesystem":
pytest.skip("Behavior depends on the filesystem.")
uid = str(uuid.uuid4())
s.upload(get_item(uid=uid.upper()))
s.upload(get_item(uid=uid.lower()))
items = list(href for href, etag in s.list())
await s.upload(get_item(uid=uid.upper()))
await s.upload(get_item(uid=uid.lower()))
items = [href async for href, etag in s.list()]
assert len(items) == 2
assert len(set(items)) == 2
def test_metadata(self, requires_metadata, s):
if not getattr(self, 'dav_server', ''):
assert not s.get_meta('color')
assert not s.get_meta('displayname')
@pytest.mark.asyncio
async def test_specialchars(
self, monkeypatch, requires_collections, get_storage_args, get_item
):
if getattr(self, "dav_server", "") in ("icloud", "fastmail"):
pytest.skip("iCloud and FastMail reject this name.")
monkeypatch.setattr("vdirsyncer.utils.generate_href", lambda x: x)
uid = "test @ foo ät bar град сатану"
collection = "test @ foo ät bar"
s = self.storage_class(**await get_storage_args(collection=collection))
item = get_item(uid=uid)
href, etag = await s.upload(item)
item2, etag2 = await s.get(href)
if etag is not None:
assert etag2 == etag
assert_item_equals(item2, item)
((_, etag3),) = await aiostream.stream.list(s.list())
assert etag2 == etag3
assert collection in urlunquote(s.collection)
if self.storage_class.storage_name.endswith("dav"):
assert urlquote(uid, "/@:") in href
@pytest.mark.asyncio
async def test_newline_in_uid(
self, monkeypatch, requires_collections, get_storage_args, get_item
):
monkeypatch.setattr("vdirsyncer.utils.generate_href", lambda x: x)
uid = "UID:20210609T084907Z-@synaps-web-54fddfdf7-7kcfm%0A.ics"
s = self.storage_class(**await get_storage_args())
item = get_item(uid=uid)
href, etag = await s.upload(item)
item2, etag2 = await s.get(href)
if etag is not None:
assert etag2 == etag
assert_item_equals(item2, item)
((_, etag3),) = await aiostream.stream.list(s.list())
assert etag2 == etag3
@pytest.mark.asyncio
async def test_empty_metadata(self, requires_metadata, s):
if getattr(self, "dav_server", ""):
pytest.skip()
assert await s.get_meta("color") is None
assert await s.get_meta("displayname") is None
@pytest.mark.asyncio
async def test_metadata(self, requires_metadata, s):
if getattr(self, "dav_server", "") == "xandikos":
pytest.skip("xandikos does not support removing metadata.")
try:
s.set_meta('color', None)
assert not s.get_meta('color')
s.set_meta('color', u'#ff0000')
assert s.get_meta('color') == u'#ff0000'
await s.set_meta("color", None)
assert await s.get_meta("color") is None
await s.set_meta("color", "#ff0000")
assert await s.get_meta("color") == "#ff0000"
except exceptions.UnsupportedMetadataError:
pass
for x in (u'hello world', u'hello wörld'):
s.set_meta('displayname', x)
rv = s.get_meta('displayname')
@pytest.mark.asyncio
async def test_encoding_metadata(self, requires_metadata, s):
for x in ("hello world", "hello wörld"):
await s.set_meta("displayname", x)
rv = await s.get_meta("displayname")
assert rv == x
assert isinstance(rv, str)
@pytest.mark.parametrize('value', [
'fööbör',
'ананасовое перо'
])
def test_metadata_normalization(self, requires_metadata, s, value):
x = s.get_meta('displayname')
@pytest.mark.parametrize(
"value",
[
None,
"",
"Hello there!",
"Österreich",
"中国",
"한글",
"42a4ec99-b1c2-4859-b142-759112f2ca50",
"فلسطين",
],
)
@pytest.mark.asyncio
async def test_metadata_normalization(self, requires_metadata, s, value):
x = await s.get_meta("displayname")
assert x == normalize_meta_value(x)
s.set_meta('displayname', value)
assert s.get_meta('displayname') == normalize_meta_value(value)
if not getattr(self, "dav_server", None):
# ownCloud replaces "" with "unnamed"
await s.set_meta("displayname", value)
assert await s.get_meta("displayname") == normalize_meta_value(value)
def test_recurring_events(self, s, item_type):
if item_type != 'VEVENT':
pytest.skip('This storage instance doesn\'t support iCalendar.')
@pytest.mark.asyncio
async def test_recurring_events(self, s, item_type):
if item_type != "VEVENT":
pytest.skip("This storage instance doesn't support iCalendar.")
uid = str(uuid.uuid4())
item = Item(textwrap.dedent(u'''
item = Item(
textwrap.dedent(
f"""
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
@ -295,7 +405,7 @@ class StorageTests(object):
BEGIN:VEVENT
DTSTART;TZID=UTC:20140128T083000Z
DTEND;TZID=UTC:20140128T100000Z
RRULE:FREQ=WEEKLY;UNTIL=20141208T213000Z;BYDAY=TU
RRULE:FREQ=WEEKLY;BYDAY=TU;UNTIL=20141208T213000Z
DTSTAMP:20140327T060506Z
UID:{uid}
CREATED:20131216T033331Z
@ -308,65 +418,11 @@ class StorageTests(object):
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
'''.format(uid=uid)).strip())
"""
).strip()
)
href, etag = s.upload(item)
href, _etag = await s.upload(item)
item2, etag2 = s.get(href)
assert item2.raw.count('BEGIN:VEVENT') == 2
assert 'RRULE' in item2.raw
def test_buffered(self, get_storage_args, get_item, requires_collections):
args = get_storage_args()
s1 = self.storage_class(**args)
s2 = self.storage_class(**args)
s1.upload(get_item())
assert sorted(list(s1.list())) == sorted(list(s2.list()))
s1.buffered()
s1.upload(get_item())
s1.flush()
assert sorted(list(s1.list())) == sorted(list(s2.list()))
def test_retain_timezones(self, item_type, s):
if item_type != 'VEVENT':
pytest.skip('This storage instance doesn\'t support iCalendar.')
item = Item(textwrap.dedent('''
BEGIN:VCALENDAR
PRODID:-//ownCloud calendar v1.4.0
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20161004T110533
DTSTAMP:20161004T110533
LAST-MODIFIED:20161004T110533
UID:y2lmgz48mg
SUMMARY:Test
CLASS:PUBLIC
STATUS:CONFIRMED
DTSTART;TZID=Europe/Berlin:20161014T101500
DTEND;TZID=Europe/Berlin:20161014T114500
END:VEVENT
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
DTSTART:20160327T030000
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20161030T020000
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
''').strip())
href, etag = s.upload(item)
item2, _ = s.get(href)
assert 'VTIMEZONE' in item2.raw
assert item2.hash == item.hash
item2, _etag2 = await s.get(href)
assert normalize_item(item) == normalize_item(item2)

View file

@ -1,36 +1,116 @@
# -*- coding: utf-8 -*-
import pytest
from __future__ import annotations
import asyncio
import contextlib
import subprocess
import time
import uuid
import aiostream
import pytest
import pytest_asyncio
import requests
@pytest.fixture
def slow_create_collection(request):
def wait_for_container(url):
"""Wait for a container to initialise.
Polls a URL every 100ms until the server responds.
"""
# give the server 5 seconds to settle
for _ in range(50):
print(_)
try:
response = requests.get(url)
response.raise_for_status()
except requests.ConnectionError:
pass
else:
return
time.sleep(0.1)
pytest.exit(
"Server did not initialise in 5 seconds.\n"
"WARNING: There may be a stale docker container still running."
)
@contextlib.contextmanager
def dockerised_server(name, container_port, exposed_port):
"""Run a dockerised DAV server as a contenxt manager."""
container_id = None
url = f"http://127.0.0.1:{exposed_port}/"
try:
# Hint: This will block while the pull happends, and only return once
# the container has actually started.
output = subprocess.check_output(
[
"docker",
"run",
"--rm",
"--detach",
"--publish",
f"{exposed_port}:{container_port}",
f"whynothugo/vdirsyncer-devkit-{name}",
]
)
container_id = output.decode().strip()
wait_for_container(url)
yield url
finally:
if container_id:
subprocess.check_output(["docker", "kill", container_id])
@pytest.fixture(scope="session")
def baikal_server():
with dockerised_server("baikal", "80", "8002"):
yield
@pytest.fixture(scope="session")
def radicale_server():
with dockerised_server("radicale", "8001", "8001"):
yield
@pytest.fixture(scope="session")
def xandikos_server():
with dockerised_server("xandikos", "8000", "8000"):
yield
@pytest_asyncio.fixture
async def slow_create_collection(request, aio_connector):
# We need to properly clean up because otherwise we might run into
# storage limits.
to_delete = []
def delete_collections():
for s in to_delete:
s.session.request('DELETE', '')
async def inner(cls: type, args: dict, collection_name: str) -> dict:
"""Create a collection
request.addfinalizer(delete_collections)
Returns args necessary to create a Storage instance pointing to it.
"""
assert collection_name.startswith("test")
def inner(cls, args, collection):
assert collection.startswith('test')
collection += '-vdirsyncer-ci-' + str(uuid.uuid4())
# Make each name unique
collection_name = f"{collection_name}-vdirsyncer-ci-{uuid.uuid4()}"
args = cls.create_collection(collection, **args)
s = cls(**args)
_clear_collection(s)
assert not list(s.list())
to_delete.append(s)
# Create the collection:
args = await cls.create_collection(collection_name, **args)
collection = cls(**args)
# Keep collection in a list to be deleted once tests end:
to_delete.append(collection)
assert not await aiostream.stream.list(collection.list())
return args
return inner
yield inner
def _clear_collection(s):
for href, etag in s.list():
s.delete(href, etag)
await asyncio.gather(*(c.session.request("DELETE", "") for c in to_delete))

View file

@ -1,25 +1,53 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import os
import uuid
from .. import StorageTests, get_server_mixin
import aiohttp
import aiostream
import pytest
from tests import assert_item_equals
from tests.storage import StorageTests
from tests.storage import get_server_mixin
from vdirsyncer import exceptions
from vdirsyncer.vobject import Item
dav_server = os.environ.get('DAV_SERVER', 'skip')
dav_server = os.environ.get("DAV_SERVER", "skip")
ServerMixin = get_server_mixin(dav_server)
class DAVStorageTests(ServerMixin, StorageTests):
dav_server = dav_server
def test_dav_empty_get_multi_performance(self, s, monkeypatch):
def breakdown(*a, **kw):
raise AssertionError('Expected not to be called.')
@pytest.mark.skipif(dav_server == "radicale", reason="Radicale is very tolerant.")
@pytest.mark.asyncio
async def test_dav_broken_item(self, s):
item = Item("HAHA:YES")
with pytest.raises((exceptions.Error, aiohttp.ClientResponseError)):
await s.upload(item)
assert not await aiostream.stream.list(s.list())
monkeypatch.setattr('requests.sessions.Session.request', breakdown)
@pytest.mark.asyncio
async def test_dav_empty_get_multi_performance(self, s, monkeypatch):
def breakdown(*a, **kw):
raise AssertionError("Expected not to be called.")
monkeypatch.setattr("requests.sessions.Session.request", breakdown)
try:
assert list(s.get_multi([])) == []
assert list(await aiostream.stream.list(s.get_multi([]))) == []
finally:
# Make sure monkeypatch doesn't interfere with DAV server teardown
monkeypatch.undo()
@pytest.mark.asyncio
async def test_dav_unicode_href(self, s, get_item, monkeypatch):
if self.dav_server == "radicale":
pytest.skip("Radicale is unable to deal with unicode hrefs")
monkeypatch.setattr(s, "_get_href", lambda item: item.ident + s.fileext)
item = get_item(uid="град сатану" + str(uuid.uuid4()))
href, _etag = await s.upload(item)
item2, _etag2 = await s.get(href)
assert_item_equals(item, item2)

View file

@ -1,43 +1,85 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import contextlib
import datetime
from textwrap import dedent
import aiohttp
import aiostream
import pytest
from aioresponses import aioresponses
from tests import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE
from tests import EVENT_TEMPLATE
from tests import TASK_TEMPLATE
from tests import VCARD_TEMPLATE
from tests.storage import format_item
from vdirsyncer import exceptions
from vdirsyncer.storage.dav import CalDAVStorage
from . import DAVStorageTests, dav_server
from .. import format_item
from . import DAVStorageTests
from . import dav_server
class TestCalDAVStorage(DAVStorageTests):
storage_class = CalDAVStorage
@pytest.fixture(params=['VTODO', 'VEVENT'])
@pytest.fixture(params=["VTODO", "VEVENT"])
def item_type(self, request):
return request.param
def test_doesnt_accept_vcard(self, item_type, get_storage_args):
s = self.storage_class(item_types=(item_type,), **get_storage_args())
@pytest.mark.asyncio
async def test_doesnt_accept_vcard(self, item_type, get_storage_args):
s = self.storage_class(item_types=(item_type,), **await get_storage_args())
try:
s.upload(format_item(item_template=VCARD_TEMPLATE))
except Exception:
pass
assert not list(s.list())
# Most storages hard-fail, but xandikos doesn't.
with contextlib.suppress(exceptions.Error, aiohttp.ClientResponseError):
await s.upload(format_item(VCARD_TEMPLATE))
@pytest.mark.xfail(dav_server == 'radicale',
reason='Radicale doesn\'t support timeranges.')
def test_timerange_correctness(self, get_storage_args):
assert not await aiostream.stream.list(s.list())
# The `arg` param is not named `item_types` because that would hit
# https://bitbucket.org/pytest-dev/pytest/issue/745/
@pytest.mark.parametrize(
("arg", "calls_num"),
[
(("VTODO",), 1),
(("VEVENT",), 1),
(("VTODO", "VEVENT"), 2),
(("VTODO", "VEVENT", "VJOURNAL"), 3),
((), 1),
],
)
@pytest.mark.xfail(dav_server == "baikal", reason="Baikal returns 500.")
@pytest.mark.asyncio
async def test_item_types_performance(
self, get_storage_args, arg, calls_num, monkeypatch
):
s = self.storage_class(item_types=arg, **await get_storage_args())
old_parse = s._parse_prop_responses
calls = []
def new_parse(*a, **kw):
calls.append(None)
return old_parse(*a, **kw)
monkeypatch.setattr(s, "_parse_prop_responses", new_parse)
await aiostream.stream.list(s.list())
assert len(calls) == calls_num
@pytest.mark.xfail(
dav_server == "radicale", reason="Radicale doesn't support timeranges."
)
@pytest.mark.asyncio
async def test_timerange_correctness(self, get_storage_args):
start_date = datetime.datetime(2013, 9, 10)
end_date = datetime.datetime(2013, 9, 13)
s = self.storage_class(start_date=start_date, end_date=end_date,
**get_storage_args())
s = self.storage_class(
start_date=start_date, end_date=end_date, **await get_storage_args()
)
too_old_item = format_item(item_template=dedent(u'''
too_old_item = format_item(
dedent(
"""
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -49,9 +91,13 @@ class TestCalDAVStorage(DAVStorageTests):
UID:{r}
END:VEVENT
END:VCALENDAR
''').strip())
"""
).strip()
)
too_new_item = format_item(item_template=dedent(u'''
too_new_item = format_item(
dedent(
"""
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -63,9 +109,13 @@ class TestCalDAVStorage(DAVStorageTests):
UID:{r}
END:VEVENT
END:VCALENDAR
''').strip())
"""
).strip()
)
good_item = format_item(item_template=dedent(u'''
good_item = format_item(
dedent(
"""
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -77,28 +127,48 @@ class TestCalDAVStorage(DAVStorageTests):
UID:{r}
END:VEVENT
END:VCALENDAR
''').strip())
"""
).strip()
)
s.upload(too_old_item)
s.upload(too_new_item)
expected_href, _ = s.upload(good_item)
await s.upload(too_old_item)
await s.upload(too_new_item)
expected_href, _ = await s.upload(good_item)
(actual_href, _), = s.list()
((actual_href, _),) = await aiostream.stream.list(s.list())
assert actual_href == expected_href
@pytest.mark.skipif(dav_server == 'icloud',
reason='iCloud only accepts VEVENT')
def test_item_types_general(self, get_storage_args):
args = get_storage_args()
s = self.storage_class(**args)
event = s.upload(format_item(item_template=EVENT_TEMPLATE))[0]
task = s.upload(format_item(item_template=TASK_TEMPLATE))[0]
@pytest.mark.asyncio
async def test_invalid_resource(self, monkeypatch, get_storage_args):
args = await get_storage_args(collection=None)
for item_types, expected_items in [
(('VTODO', 'VEVENT'), {event, task}),
(('VTODO',), {task}),
(('VEVENT',), {event}),
]:
args['item_types'] = item_types
s = self.storage_class(**args)
assert set(href for href, etag in s.list()) == expected_items
with aioresponses() as m:
m.add(args["url"], method="PROPFIND", status=200, body="Hello world")
with pytest.raises(ValueError):
s = self.storage_class(**args)
await aiostream.stream.list(s.list())
assert len(m.requests) == 1
@pytest.mark.skipif(dav_server == "icloud", reason="iCloud only accepts VEVENT")
@pytest.mark.skipif(
dav_server == "fastmail", reason="Fastmail has non-standard hadling of VTODOs."
)
@pytest.mark.xfail(dav_server == "baikal", reason="Baikal returns 500.")
@pytest.mark.asyncio
async def test_item_types_general(self, s):
event = (await s.upload(format_item(EVENT_TEMPLATE)))[0]
task = (await s.upload(format_item(TASK_TEMPLATE)))[0]
s.item_types = ("VTODO", "VEVENT")
async def hrefs():
return {href async for href, etag in s.list()}
assert await hrefs() == {event, task}
s.item_types = ("VTODO",)
assert await hrefs() == {task}
s.item_types = ("VEVENT",)
assert await hrefs() == {event}
s.item_types = ()
assert await hrefs() == {event, task}

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import pytest
@ -10,6 +10,6 @@ from . import DAVStorageTests
class TestCardDAVStorage(DAVStorageTests):
storage_class = CardDAVStorage
@pytest.fixture(params=['VCARD'])
@pytest.fixture(params=["VCARD"])
def item_type(self, request):
return request.param

View file

@ -1,40 +1,59 @@
from __future__ import annotations
import pytest
from vdirsyncer.storage.dav import _BAD_XML_CHARS, _merge_xml, _parse_xml
from vdirsyncer.storage.dav import _BAD_XML_CHARS
from vdirsyncer.storage.dav import _merge_xml
from vdirsyncer.storage.dav import _normalize_href
from vdirsyncer.storage.dav import _parse_xml
def test_xml_utilities():
x = _parse_xml(b'''<?xml version="1.0" encoding="UTF-8" ?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:propstat>
<D:status>HTTP/1.1 404 Not Found</D:status>
<D:prop>
<D:getcontenttype/>
</D:prop>
</D:propstat>
<D:propstat>
<D:prop>
<D:resourcetype>
<D:collection/>
</D:resourcetype>
</D:prop>
</D:propstat>
</D:response>
</D:multistatus>
''')
x = _parse_xml(
b"""<?xml version="1.0" encoding="UTF-8" ?>
<multistatus xmlns="DAV:">
<response>
<propstat>
<status>HTTP/1.1 404 Not Found</status>
<prop>
<getcontenttype/>
</prop>
</propstat>
<propstat>
<prop>
<resourcetype>
<collection/>
</resourcetype>
</prop>
</propstat>
</response>
</multistatus>
"""
)
response = x.find('{DAV:}response')
props = _merge_xml(response.findall('{DAV:}propstat/{DAV:}prop'))
assert props.find('{DAV:}resourcetype/{DAV:}collection') is not None
assert props.find('{DAV:}getcontenttype') is not None
response = x.find("{DAV:}response")
props = _merge_xml(response.findall("{DAV:}propstat/{DAV:}prop"))
assert props.find("{DAV:}resourcetype/{DAV:}collection") is not None
assert props.find("{DAV:}getcontenttype") is not None
@pytest.mark.parametrize('char', range(32))
@pytest.mark.parametrize("char", range(32))
def test_xml_specialchars(char):
x = _parse_xml('<?xml version="1.0" encoding="UTF-8" ?>'
'<foo>ye{}s\r\n'
'hello</foo>'.format(chr(char)).encode('ascii'))
x = _parse_xml(
'<?xml version="1.0" encoding="UTF-8" ?>'
f"<foo>ye{chr(char)}s\r\n"
"hello</foo>".encode("ascii")
)
if char in _BAD_XML_CHARS:
assert x.text == 'yes\nhello'
assert x.text == "yes\nhello"
@pytest.mark.parametrize(
"href",
[
"/dav/calendars/user/testuser/123/UID%253A20210609T084907Z-@synaps-web-54fddfdf7-7kcfm%250A.ics",
],
)
def test_normalize_href(href):
assert href == _normalize_href("https://example.com", href)

View file

@ -1,124 +0,0 @@
"""
Django settings for etesync_server project.
Generated by 'django-admin startproject' using Django 1.10.6.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'd7r(p-9=$3a@bbt%*+$p@4)cej13nzd0gmnt8+m0bitb=-umj#'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'journal.apps.JournalConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'etesync_server.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'etesync_server.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.environ.get('ETESYNC_DB_PATH',
os.path.join(BASE_DIR, 'db.sqlite3')),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'

View file

@ -1,41 +0,0 @@
"""etesync_server URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import include, url
from rest_framework_nested import routers
from journal import views
router = routers.DefaultRouter()
router.register(r'journals', views.JournalViewSet)
router.register(r'journal/(?P<journal_uid>[^/]+)', views.EntryViewSet)
router.register(r'user', views.UserInfoViewSet)
journals_router = routers.NestedSimpleRouter(router, r'journals',
lookup='journal')
journals_router.register(r'members', views.MembersViewSet,
base_name='journal-members')
journals_router.register(r'entries', views.EntryViewSet,
base_name='journal-entries')
urlpatterns = [
url(r'^api/v1/', include(router.urls)),
url(r'^api/v1/', include(journals_router.urls)),
]
# Adding this just for testing, this shouldn't be here normally
urlpatterns += url(r'^reset/$', views.reset, name='reset_debug'),

View file

@ -1,16 +0,0 @@
"""
WSGI config for etesync_server project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etesync_server.settings")
application = get_wsgi_application()

View file

@ -1,22 +0,0 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etesync_server.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django # noqa
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

View file

@ -1 +0,0 @@
63ae6eec45b592d5c511f79b7b0c312d2c5f7d6a

View file

@ -1,92 +0,0 @@
# -*- coding: utf-8 -*-
import shutil
import os
import sys
import pytest
import requests
from vdirsyncer.storage.etesync import EtesyncContacts, EtesyncCalendars
from .. import StorageTests
pytestmark = pytest.mark.skipif(os.getenv('ETESYNC_TESTS', '') != 'true',
reason='etesync tests disabled')
@pytest.fixture(scope='session')
def etesync_app(tmpdir_factory):
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
'etesync_server'))
db = tmpdir_factory.mktemp('etesync').join('etesync.sqlite')
shutil.copy(
os.path.join(os.path.dirname(__file__), 'etesync_server',
'db.sqlite3'),
str(db)
)
os.environ['ETESYNC_DB_PATH'] = str(db)
from etesync_server.wsgi import application
return application
class EtesyncTests(StorageTests):
supports_metadata = False
@pytest.fixture
def get_storage_args(self, request, get_item, tmpdir, etesync_app):
import wsgi_intercept
import wsgi_intercept.requests_intercept
wsgi_intercept.requests_intercept.install()
wsgi_intercept.add_wsgi_intercept('127.0.0.1', 8000,
lambda: etesync_app)
def teardown():
wsgi_intercept.remove_wsgi_intercept('127.0.0.1', 8000)
wsgi_intercept.requests_intercept.uninstall()
request.addfinalizer(teardown)
with open(os.path.join(os.path.dirname(__file__),
'test@localhost/auth_token')) as f:
token = f.read().strip()
headers = {'Authorization': 'Token ' + token}
r = requests.post('http://127.0.0.1:8000/reset/', headers=headers,
allow_redirects=False)
assert r.status_code == 200
def inner(collection='test'):
rv = {
'email': 'test@localhost',
'db_path': str(tmpdir.join('etesync.db')),
'secrets_dir': os.path.dirname(__file__),
'server_url': 'http://127.0.0.1:8000/'
}
if collection is not None:
rv = self.storage_class.create_collection(
collection=collection,
**rv
)
return rv
return inner
class TestContacts(EtesyncTests):
storage_class = EtesyncContacts
@pytest.fixture(params=['VCARD'])
def item_type(self, request):
return request.param
class TestCalendars(EtesyncTests):
storage_class = EtesyncCalendars
@pytest.fixture(params=['VEVENT'])
def item_type(self, request):
return request.param

View file

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,38 @@
from __future__ import annotations
import pytest
class ServerMixin:
@pytest.fixture
def get_storage_args(
self,
request,
tmpdir,
slow_create_collection,
baikal_server,
aio_connector,
):
async def inner(collection="test"):
base_url = "http://127.0.0.1:8002/"
args = {
"url": base_url,
"username": "baikal",
"password": "baikal",
"connector": aio_connector,
}
if self.storage_class.fileext == ".vcf":
args["url"] = base_url + "card.php/"
else:
args["url"] = base_url + "cal.php/"
if collection is not None:
args = await slow_create_collection(
self.storage_class,
args,
collection,
)
return args
return inner

View file

@ -1,49 +1,50 @@
from __future__ import annotations
import os
import pytest
import uuid
import pytest
try:
caldav_args = {
# Those credentials are configured through the Travis UI
'username': os.environ['DAVICAL_USERNAME'].strip(),
'password': os.environ['DAVICAL_PASSWORD'].strip(),
'url': 'https://caesar.lostpackets.de/davical-test/caldav.php/',
"username": os.environ["DAVICAL_USERNAME"].strip(),
"password": os.environ["DAVICAL_PASSWORD"].strip(),
"url": "https://brutus.lostpackets.de/davical-test/caldav.php/",
}
except KeyError as e:
caldav_args = None
pytestmark = pytest.mark.skip(f"Missing envkey: {e!s}")
@pytest.mark.flaky(reruns=5)
class ServerMixin(object):
class ServerMixin:
@pytest.fixture
def davical_args(self):
if caldav_args is None:
pytest.skip('Missing envkeys for davical')
if self.storage_class.fileext == '.ics':
if self.storage_class.fileext == ".ics":
return dict(caldav_args)
elif self.storage_class.fileext == '.vcf':
pytest.skip('No carddav')
elif self.storage_class.fileext == ".vcf":
pytest.skip("No carddav")
else:
raise RuntimeError()
raise RuntimeError
@pytest.fixture
def get_storage_args(self, davical_args, request):
def inner(collection='test'):
async def inner(collection="test"):
if collection is None:
return davical_args
assert collection.startswith('test')
assert collection.startswith("test")
for _ in range(4):
args = self.storage_class.create_collection(
collection + str(uuid.uuid4()),
**davical_args
collection + str(uuid.uuid4()), **davical_args
)
s = self.storage_class(**args)
if not list(s.list()):
request.addfinalizer(
lambda: s.session.request('DELETE', ''))
# See: https://stackoverflow.com/a/33984811
request.addfinalizer(lambda x=s: x.session.request("DELETE", ""))
return args
raise RuntimeError('Failed to find free collection.')
raise RuntimeError("Failed to find free collection.")
return inner

View file

@ -1,31 +1,42 @@
from __future__ import annotations
import os
import pytest
username = os.environ.get('FASTMAIL_USERNAME', '').strip()
password = os.environ.get('FASTMAIL_PASSWORD', '').strip()
class ServerMixin(object):
class ServerMixin:
@pytest.fixture
def get_storage_args(self, slow_create_collection):
if not username:
pytest.skip('Fastmail credentials not available')
def get_storage_args(self, slow_create_collection, aio_connector, request):
if (
"item_type" in request.fixturenames
and request.getfixturevalue("item_type") == "VTODO"
):
# Fastmail has non-standard support for TODOs
# See https://github.com/pimutils/vdirsyncer/issues/824
pytest.skip("Fastmail has non-standard VTODO support.")
def inner(collection='test'):
args = {'username': username, 'password': password}
async def inner(collection="test"):
args = {
"username": os.environ["FASTMAIL_USERNAME"],
"password": os.environ["FASTMAIL_PASSWORD"],
"connector": aio_connector,
}
if self.storage_class.fileext == '.ics':
args['url'] = 'https://caldav.messagingengine.com/'
elif self.storage_class.fileext == '.vcf':
args['url'] = 'https://carddav.messagingengine.com/'
if self.storage_class.fileext == ".ics":
args["url"] = "https://caldav.fastmail.com/"
elif self.storage_class.fileext == ".vcf":
args["url"] = "https://carddav.fastmail.com/"
else:
raise RuntimeError()
raise RuntimeError
if collection is not None:
args = slow_create_collection(self.storage_class, args,
collection)
args = await slow_create_collection(
self.storage_class,
args,
collection,
)
return args
return inner

View file

@ -1,35 +1,33 @@
from __future__ import annotations
import os
import pytest
username = os.environ.get('ICLOUD_USERNAME', '').strip()
password = os.environ.get('ICLOUD_PASSWORD', '').strip()
class ServerMixin(object):
class ServerMixin:
@pytest.fixture
def get_storage_args(self, item_type, slow_create_collection):
if item_type != 'VEVENT':
if item_type != "VEVENT":
# iCloud collections can either be calendars or task lists.
# See https://github.com/pimutils/vdirsyncer/pull/593#issuecomment-285941615 # noqa
pytest.skip('iCloud doesn\'t support anything else than VEVENT')
# See https://github.com/pimutils/vdirsyncer/pull/593#issuecomment-285941615
pytest.skip("iCloud doesn't support anything else than VEVENT")
if not username:
pytest.skip('iCloud credentials not available')
async def inner(collection="test"):
args = {
"username": os.environ["ICLOUD_USERNAME"],
"password": os.environ["ICLOUD_PASSWORD"],
}
def inner(collection='test'):
args = {'username': username, 'password': password}
if self.storage_class.fileext == '.ics':
args['url'] = 'https://caldav.icloud.com/'
elif self.storage_class.fileext == '.vcf':
args['url'] = 'https://contacts.icloud.com/'
if self.storage_class.fileext == ".ics":
args["url"] = "https://caldav.icloud.com/"
elif self.storage_class.fileext == ".vcf":
args["url"] = "https://contacts.icloud.com/"
else:
raise RuntimeError()
raise RuntimeError
if collection is not None:
args = slow_create_collection(self.storage_class, args,
collection)
args = slow_create_collection(self.storage_class, args, collection)
return args
return inner

View file

@ -1,29 +0,0 @@
import os
import requests
import pytest
port = os.environ.get('NEXTCLOUD_HOST', None) or 'localhost:5000'
user = os.environ.get('NEXTCLOUD_USER', None) or 'asdf'
pwd = os.environ.get('NEXTCLOUD_PASS', None) or 'asdf'
class ServerMixin(object):
storage_class = None
wsgi_teardown = None
@pytest.fixture
def get_storage_args(self, item_type,
slow_create_collection):
def inner(collection='test'):
args = {
'username': user,
'password': pwd,
'url': 'http://{}/remote.php/dav/'.format(port)
}
if collection is not None:
args = slow_create_collection(self.storage_class, args,
collection)
return args
return inner

View file

@ -1,59 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
import pytest
import radicale
import radicale.config
from pkg_resources import parse_version as ver
import wsgi_intercept
import wsgi_intercept.requests_intercept
logger = logging.getLogger(__name__)
class ServerMixin(object):
@pytest.fixture(autouse=True)
def setup(self, request, tmpdir):
if ver(radicale.VERSION) < ver('2.0.0-pre'):
raise RuntimeError('Testing against Radicale only works with '
'Radicale >= 2.0.0')
def get_app():
config = radicale.config.load(())
config.set('storage', 'filesystem_folder', str(tmpdir))
config.set('rights', 'type', 'owner_only')
app = radicale.Application(config, logger)
def is_authenticated(user, password):
return user == 'bob' and password == 'bob'
app.is_authenticated = is_authenticated
return app
wsgi_intercept.requests_intercept.install()
wsgi_intercept.add_wsgi_intercept('127.0.0.1', 80, get_app)
def teardown():
wsgi_intercept.remove_wsgi_intercept('127.0.0.1', 80)
wsgi_intercept.requests_intercept.uninstall()
request.addfinalizer(teardown)
class ServerMixin:
@pytest.fixture
def get_storage_args(self, get_item):
def inner(collection='test'):
url = 'http://127.0.0.1/'
rv = {'url': url, 'username': 'bob', 'password': 'bob'}
def get_storage_args(
self,
request,
tmpdir,
slow_create_collection,
radicale_server,
aio_connector,
):
async def inner(collection="test"):
url = "http://127.0.0.1:8001/"
args = {
"url": url,
"username": "radicale",
"password": "radicale",
"connector": aio_connector,
}
if collection is not None:
collection = collection + self.storage_class.fileext
rv = self.storage_class.create_collection(collection, **rv)
s = self.storage_class(**rv)
assert not list(s.list())
args = await slow_create_collection(
self.storage_class,
args,
collection,
)
return args
return rv
return inner

View file

@ -1,12 +0,0 @@
#!/bin/sh
set -e
if [ "$REQUIREMENTS" = "release" ] || [ "$REQUIREMENTS" = "minimal" ]; then
radicale_pkg="radicale"
elif [ "$REQUIREMENTS" = "devel" ]; then
radicale_pkg="git+https://github.com/Kozea/Radicale.git"
else
echo "Invalid requirements envvar"
false
fi
pip install wsgi_intercept $radicale_pkg

Some files were not shown because too many files have changed in this diff Show more