Compare commits

...

28 commits

Author SHA1 Message Date
Markus Unterwaditzer
7fdff404e6 No wheels 2017-12-04 20:16:29 +01:00
Markus Unterwaditzer
1bdde25c0c Fix etesync build 2017-12-04 19:52:02 +01:00
Markus Unterwaditzer
b32932bd13 Relax recurrence tests 2017-12-03 14:00:21 +01:00
Markus Unterwaditzer
22d009b824 Remove unnecessary filter 2017-11-27 19:52:15 +01:00
Markus Unterwaditzer
792dbc171f Fix missing XML header, see #688 2017-11-25 14:15:14 +01:00
Markus Unterwaditzer
5700c4688b
rustup (#686)
* rustup

* rust-vobject upgrade
2017-11-07 21:58:17 +01:00
Markus Unterwaditzer
3984f547ce
Update nextcloud (#684) 2017-11-05 15:59:42 +01:00
Markus Unterwaditzer
9769dab02e
Update owncloud (#685) 2017-11-05 15:59:34 +01:00
Markus Unterwaditzer
bd2e09a84b Small refactor in dav.py 2017-10-26 02:22:18 +02:00
Markus Unterwaditzer
f7b6e67095 Ignore new flake8 linters 2017-10-26 01:41:43 +02:00
Markus Unterwaditzer
a2c509adf5 rustup, fix broken struct export 2017-10-25 22:36:28 +02:00
Markus Unterwaditzer
28fdf42238 Fix #681 2017-10-21 17:23:41 +02:00
Markus Unterwaditzer
0d3b028b17 Cache rust artifacts 2017-10-19 23:47:20 +02:00
Markus Unterwaditzer
f8e65878d8 Update rust installation instructions 2017-10-19 23:41:43 +02:00
Markus Unterwaditzer
75e83cd0f6 Commit cargo.lock 2017-10-19 23:27:29 +02:00
Malte Kiefer
96a8ab35c3 fixed typo (#678)
fixed typo
2017-10-13 19:34:37 +02:00
Markus Unterwaditzer
619373a8e8 Rust: new item module 2017-10-11 13:53:10 +02:00
Markus Unterwaditzer
cbb15e1895 Move all target to top again 2017-10-11 13:28:00 +02:00
Markus Unterwaditzer
325304c50f Lazy-load component in item 2017-10-11 12:01:52 +02:00
Markus Unterwaditzer
bdbfc360ff Move item hashing into rust 2017-10-10 00:52:58 +02:00
Markus Unterwaditzer
c17fa308fb Adapt virtualenv steps to always select python3 2017-10-06 18:32:17 +02:00
Markus Unterwaditzer
81f7472e3a Update installation instructions for Rust dependencies 2017-10-06 18:30:10 +02:00
Markus Unterwaditzer
69543b8615 Install rust on readthedocs 2017-10-05 17:45:19 +02:00
Markus Unterwaditzer
1b7cb4e656 Use rust-vobject (#675)
Use rust-vobject
2017-10-04 22:41:18 +02:00
Markus Unterwaditzer
7bdb22a207 Fix Ubuntu package name of Python 3. 2017-10-03 22:48:13 +02:00
Markus Unterwaditzer
cb41a9df28 Add fast_finish to Travis 2017-10-03 20:59:43 +02:00
Markus Unterwaditzer
33f96f5eca Fix broken link 2017-10-03 13:13:44 +02:00
Markus Unterwaditzer
178ac237ad Fix installation link 2017-10-03 11:29:51 +02:00
34 changed files with 1043 additions and 279 deletions

1
.gitignore vendored
View file

@ -12,4 +12,5 @@ env
dist dist
docs/_build/ docs/_build/
vdirsyncer/version.py vdirsyncer/version.py
vdirsyncer/_native*
.hypothesis .hypothesis

View file

@ -5,7 +5,12 @@
"master" "master"
] ]
}, },
"cache": "pip", "cache": {
"directories": [
"./rust/target/"
],
"pip": true
},
"dist": "trusty", "dist": "trusty",
"git": { "git": {
"submodules": false "submodules": false
@ -19,6 +24,7 @@
], ],
"language": "python", "language": "python",
"matrix": { "matrix": {
"fast_finish": true,
"include": [ "include": [
{ {
"env": "BUILD=style", "env": "BUILD=style",

View file

@ -9,6 +9,12 @@ Package maintainers and users who have to manually update their installation
may want to subscribe to `GitHub's tag feed may want to subscribe to `GitHub's tag feed
<https://github.com/pimutils/vdirsyncer/tags.atom>`_. <https://github.com/pimutils/vdirsyncer/tags.atom>`_.
Version 0.17.0
==============
- Fix bug where collection discovery under DAV-storages would produce invalid
XML. See :gh:`688`.
Version 0.16.3 Version 0.16.3
============== ==============

View file

@ -36,8 +36,7 @@ ifeq ($(COVERAGE), true)
endif endif
ifeq ($(ETESYNC_TESTS), true) ifeq ($(ETESYNC_TESTS), true)
TEST_EXTRA_PACKAGES += git+https://github.com/etesync/journal-manager TEST_EXTRA_PACKAGES += django-etesync-journal django djangorestframework wsgi_intercept drf-nested-routers
TEST_EXTRA_PACKAGES += django djangorestframework wsgi_intercept drf-nested-routers
endif endif
PYTEST = py.test $(PYTEST_ARGS) PYTEST = py.test $(PYTEST_ARGS)
@ -45,6 +44,9 @@ PYTEST = py.test $(PYTEST_ARGS)
export TESTSERVER_BASE := ./tests/storage/servers/ export TESTSERVER_BASE := ./tests/storage/servers/
CODECOV_PATH = /tmp/codecov.sh CODECOV_PATH = /tmp/codecov.sh
all:
$(error Take a look at https://vdirsyncer.pimutils.org/en/stable/tutorial.html#installation)
ifeq ($(CI), true) ifeq ($(CI), true)
test: test:
curl -s https://codecov.io/bash > $(CODECOV_PATH) curl -s https://codecov.io/bash > $(CODECOV_PATH)
@ -59,9 +61,6 @@ test:
$(PYTEST) $(PYTEST)
endif endif
all:
$(error Take a look at https://vdirsyncer.pimutils.org/en/stable/tutorial.html#installation)
install-servers: install-servers:
set -ex; \ set -ex; \
for server in $(DAV_SERVER); do \ for server in $(DAV_SERVER); do \
@ -104,7 +103,7 @@ linkcheck:
sphinx-build -W -b linkcheck ./docs/ ./docs/_build/linkcheck/ sphinx-build -W -b linkcheck ./docs/ ./docs/_build/linkcheck/
release: release:
python setup.py sdist bdist_wheel upload python setup.py sdist upload
release-deb: release-deb:
sh scripts/release-deb.sh debian jessie sh scripts/release-deb.sh debian jessie
@ -114,7 +113,7 @@ release-deb:
sh scripts/release-deb.sh ubuntu zesty sh scripts/release-deb.sh ubuntu zesty
install-dev: install-dev:
pip install -e . pip install -ve .
[ "$(ETESYNC_TESTS)" = "false" ] || pip install -e .[etesync] [ "$(ETESYNC_TESTS)" = "false" ] || pip install -e .[etesync]
set -xe && if [ "$(REQUIREMENTS)" = "devel" ]; then \ set -xe && if [ "$(REQUIREMENTS)" = "devel" ]; then \
pip install -U --force-reinstall \ pip install -U --force-reinstall \
@ -139,4 +138,11 @@ ssh-submodule-urls:
echo -n 'New URL: '; \ echo -n 'New URL: '; \
git remote get-url origin" git remote get-url origin"
install-rust:
curl https://sh.rustup.rs -sSf | sh -s -- -y
rust-ext:
[ "$$READTHEDOCS" != "True" ] || $(MAKE) install-rust
cd ./rust && PATH="$$HOME/.cargo/bin/:$$PATH" cargo build --release
.PHONY: docs .PHONY: docs

View file

@ -12,7 +12,7 @@ writing:
- `ArchLinux (AUR) <https://aur.archlinux.org/packages/vdirsyncer>`_ - `ArchLinux (AUR) <https://aur.archlinux.org/packages/vdirsyncer>`_
- `Ubuntu and Debian, x86_64-only - `Ubuntu and Debian, x86_64-only
<https://packagecloud.io/pimutils/vdirsyncer/install>`_ (packages also exist <https://packagecloud.io/pimutils/vdirsyncer>`_ (packages also exist
in the official repositories but may be out of date) in the official repositories but may be out of date)
- `GNU Guix <https://www.gnu.org/software/guix/package-list.html#vdirsyncer>`_ - `GNU Guix <https://www.gnu.org/software/guix/package-list.html#vdirsyncer>`_
- `OS X (homebrew) <http://braumeister.org/formula/vdirsyncer>`_ - `OS X (homebrew) <http://braumeister.org/formula/vdirsyncer>`_
@ -44,12 +44,17 @@ following things are installed:
- Python 3.4+ and pip. - Python 3.4+ and pip.
- ``libxml`` and ``libxslt`` - ``libxml`` and ``libxslt``
- ``zlib`` - ``zlib``
- Linux or OS X. **Windows is not supported, see :gh:`535`.** - `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`.
On Linux systems, using the distro's package manager is the best On Linux systems, using the distro's package manager is the best way to do
way to do this, for example, using Ubuntu:: this, for example, using Ubuntu (last tried on Trusty)::
sudo apt-get install libxml2 libxslt1.1 zlib1g python 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.
Then you have several options. The following text applies for most Python Then you have several options. The following text applies for most Python
software by the way. software by the way.
@ -59,11 +64,14 @@ The dirty, easy way
The easiest way to install vdirsyncer at this point would be to run:: The easiest way to install vdirsyncer at this point would be to run::
pip install --user --ignore-installed vdirsyncer pip3 install -v --user --ignore-installed vdirsyncer
- ``--user`` is to install without root rights (into your home directory) - ``--user`` is to install without root rights (into your home directory)
- ``--ignore-installed`` is to work around Debian's potentially broken packages - ``--ignore-installed`` is to work around Debian's potentially broken packages
(see :ref:`debian-urllib3`). (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/``.
This method has a major flaw though: Pip doesn't keep track of the files it 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 installs. Vdirsyncer's files would be located somewhere in
@ -79,9 +87,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, your filesystem: virtualenv_. There are a lot of resources on how to use it,
the simplest possible way would look something like:: the simplest possible way would look something like::
virtualenv ~/vdirsyncer_env virtualenv --python python3 ~/vdirsyncer_env
~/vdirsyncer_env/bin/pip install vdirsyncer ~/vdirsyncer_env/bin/pip install -v vdirsyncer
alias vdirsyncer="~/vdirsyncer_env/bin/vdirsyncer alias vdirsyncer="$HOME/vdirsyncer_env/bin/vdirsyncer"
You'll have to put the last line into your ``.bashrc`` or ``.bash_profile``. You'll have to put the last line into your ``.bashrc`` or ``.bash_profile``.

View file

@ -86,7 +86,7 @@ Crontab
On the end we create a crontab, so that vdirsyncer syncs automatically On the end we create a crontab, so that vdirsyncer syncs automatically
every 30 minutes our contacts:: every 30 minutes our contacts::
contab -e crontab -e
On the end of that file enter this line:: On the end of that file enter this line::

1
rust/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

454
rust/Cargo.lock generated Normal file
View file

@ -0,0 +1,454 @@
[root]
name = "vdirsyncer_rustext"
version = "0.1.0"
dependencies = [
"cbindgen 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
"ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
"vobject 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ansi_term"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "atty"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "backtrace"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "backtrace-sys"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bitflags"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bitflags"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cbindgen"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"clap 2.27.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cc"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "clap"
version = "2.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "coco"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"either 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dbghelp-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dtoa"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "either"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "error-chain"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fuchsia-zircon"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "futures"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "gcc"
version = "0.3.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "itoa"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lazy_static"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "log"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num-traits"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num_cpus"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rand"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rayon"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rayon-core 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rayon-core"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ring"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"untrusted 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-demangle"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "scopeguard"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_codegen_internals"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_derive"
version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_codegen_internals 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_json"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "strsim"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "syn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "synom"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tempdir"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termion"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "textwrap"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "toml"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-width"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-xid"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "untrusted"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "vec_map"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "vobject"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
"checksum atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "21e50800ec991574876040fff8ee46b136a53e985286fbe6a3bdfe6421b78860"
"checksum backtrace 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "99f2ce94e22b8e664d95c57fff45b98a966c2252b60691d0b7aeeccd88d70983"
"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661"
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum cbindgen 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "a4f329332ea243c7bf4ca3fc8a0c35884ab6de60b4c4a2bb89238ec6947a75cb"
"checksum cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a9b13a57efd6b30ecd6598ebdb302cca617930b5470647570468a65d12ef9719"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
"checksum clap 2.27.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b8c532887f1a292d17de05ae858a8fe50a301e196f9ef0ddb7ccd0d1d00f180"
"checksum coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c06169f5beb7e31c7c67ebf5540b8b472d23e3eade3b2ec7d1f5b504a85f91bd"
"checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
"checksum either 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e311a7479512fbdf858fb54d91ec59f3b9f85bc0113659f46bba12b199d273ce"
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
"checksum fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f6c0581a4e363262e52b87f59ee2afe3415361c6ec35e665924eb08afe8ff159"
"checksum fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "43f3795b4bae048dc6123a6b972cadde2e676f9ded08aef6bb77f5f157684a82"
"checksum futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "118b49cac82e04121117cbd3121ede3147e885627d82c4546b87c702debb90c1"
"checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb"
"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c9e5e58fa1a4c3b915a561a78a22ee0cac6ab97dca2504428bc1cb074375f8d5"
"checksum libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "5ba3df4dcb460b9dfbd070d41c94c19209620c191b0340b929ce748a2bcd42d2"
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
"checksum num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "99843c856d68d8b4313b03a17e33c4bb42ae8f6610ea81b28abe076ac721b9b0"
"checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum rand 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "61efcbcd9fa8d8fbb07c84e34a8af18a1ff177b449689ad38a6e9457ecc7b2ae"
"checksum rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b614fe08b6665cb9a231d07ac1364b0ef3cb3698f1239ee0c4c3a88a524f54c8"
"checksum rayon-core 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7febc28567082c345f10cddc3612c6ea020fc3297a1977d472cf9fdb73e6e493"
"checksum redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "8dde11f18c108289bef24469638a04dce49da56084f2d50618b226e47eb04509"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6f7d28b30a72c01b458428e0ae988d4149c20d902346902be881e3edc4bb325c"
"checksum rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aee45432acc62f7b9a108cc054142dac51f979e69e71ddce7d6fc7adf29e817e"
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)" = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af"
"checksum serde_codegen_internals 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bc888bd283bd2420b16ad0d860e35ad8acb21941180a83a189bb2046f9d00400"
"checksum serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)" = "978fd866f4d4872084a81ccc35e275158351d3b9fe620074e7d7504b816b74ba"
"checksum serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ad8bcf487be7d2e15d3d543f04312de991d631cfe1b43ea0ade69e6a8a5b16a1"
"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
"checksum toml 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bd86ad9ebee246fdedd610e0f6d0587b754a3d81438db930a244d0480ed7878f"
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
"checksum untrusted 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f392d7819dbe58833e26872f5f6f0d68b7bbbe90fc3667e98731c4a15ad9a7ae"
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
"checksum vobject 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9ce763ee7201cb2915eb28450a2a1361e6fdc1650ff2ed806aa3d3216df92141"
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"

16
rust/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "vdirsyncer_rustext"
version = "0.1.0"
authors = ["Markus Unterwaditzer <markus@unterwaditzer.net>"]
build = "build.rs"
[lib]
name = "vdirsyncer_rustext"
crate-type = ["cdylib"]
[dependencies]
vobject = "0.4.0"
ring = "0.12.1"
[build-dependencies]
cbindgen = "0.1"

12
rust/build.rs Normal file
View file

@ -0,0 +1,12 @@
extern crate cbindgen;
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let mut config: cbindgen::Config = Default::default();
config.language = cbindgen::Language::C;
cbindgen::generate_with_config(&crate_dir, config)
.unwrap()
.write_to_file("target/vdirsyncer_rustext.h");
}

145
rust/src/item.rs Normal file
View file

@ -0,0 +1,145 @@
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::mem;
use std::ptr;
use vobject;
use ring;
use std::fmt::Write;
use VdirsyncerError;
const EMPTY_STRING: *const c_char = b"\0" as *const u8 as *const c_char;
pub struct VdirsyncerComponent(vobject::Component);
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_get_uid(c: *mut VdirsyncerComponent) -> *const c_char {
match safe_get_uid(&(*c).0) {
Some(x) => CString::new(x).unwrap().into_raw(),
None => EMPTY_STRING
}
}
#[inline]
fn safe_get_uid(c: &vobject::Component) -> Option<String> {
let mut stack = 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
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_parse_component(s: *const c_char, err: *mut VdirsyncerError) -> *mut VdirsyncerComponent {
let cstring = CStr::from_ptr(s);
match vobject::parse_component(cstring.to_str().unwrap()) {
Ok(x) => mem::transmute(Box::new(VdirsyncerComponent(x))),
Err(e) => {
(*err).failed = true;
(*err).msg = CString::new(e.description()).unwrap().into_raw();
mem::zeroed()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_component(c: *mut VdirsyncerComponent) {
let _: Box<VdirsyncerComponent> = mem::transmute(c);
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_clear_err(e: *mut VdirsyncerError) {
CString::from_raw((*e).msg);
(*e).msg = ptr::null_mut();
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_change_uid(c: *mut VdirsyncerComponent, uid: *const c_char) {
let uid_cstring = CStr::from_ptr(uid);
change_uid(&mut (*c).0, uid_cstring.to_str().unwrap());
}
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());
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_clone_component(c: *mut VdirsyncerComponent) -> *mut VdirsyncerComponent {
mem::transmute(Box::new((*c).0.clone()))
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_write_component(c: *mut VdirsyncerComponent) -> *const c_char {
CString::new(vobject::write_component(&(*c).0)).unwrap().into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_hash_component(c: *mut VdirsyncerComponent) -> *const c_char {
CString::new(safe_hash_component(&(*c).0)).unwrap().into_raw()
}
fn safe_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");
// Apparently this is set by Horde?
// https://github.com/pimutils/vdirsyncer/issues/318
component.remove("X-WR-CALNAME");
// 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" {
component.subcomponents.retain(|ref c| c.name != "VTIMEZONE");
}
stack.extend(component.subcomponents.iter_mut());
}
}
// FIXME: Possible optimization: Stream component to hasher instead of allocating new string
let digest = ring::digest::digest(&ring::digest::SHA256, vobject::write_component(&new_c).as_bytes());
let mut rv = String::new();
for &byte in digest.as_ref() {
write!(&mut rv, "{:x}", byte).unwrap();
}
rv
}

19
rust/src/lib.rs Normal file
View file

@ -0,0 +1,19 @@
extern crate vobject;
extern crate ring;
use std::ffi::CStr;
use std::os::raw::c_char;
pub mod item;
#[repr(C)]
pub struct VdirsyncerError {
pub failed: bool,
pub msg: *mut c_char,
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_str(s: *const c_char) {
CStr::from_ptr(s);
}

View file

@ -8,9 +8,12 @@ ARG distrover
RUN apt-get update RUN apt-get update
RUN apt-get install -y build-essential fakeroot debhelper git RUN apt-get install -y build-essential fakeroot debhelper git
RUN apt-get install -y python3-all python3-pip RUN apt-get install -y python3-all python3-dev python3-pip
RUN apt-get install -y ruby ruby-dev RUN apt-get install -y ruby ruby-dev
RUN apt-get install -y python-all python-pip 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 gem install fpm
@ -24,7 +27,7 @@ RUN mkdir /vdirsyncer/pkgs/
RUN basename *.tar.gz .tar.gz | cut -d'-' -f2 | sed -e 's/\.dev/~/g' | tee version 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 (echo -n *.tar.gz; echo '[google]') | tee requirements.txt
RUN . /vdirsyncer/env/bin/activate; fpm -s virtualenv -t deb \ RUN . /vdirsyncer/env/bin/activate; fpm --verbose -s virtualenv -t deb \
-n "vdirsyncer-latest" \ -n "vdirsyncer-latest" \
-v "$(cat version)" \ -v "$(cat version)" \
--prefix /opt/venvs/vdirsyncer-latest \ --prefix /opt/venvs/vdirsyncer-latest \

View file

@ -10,7 +10,12 @@ cfg = {}
cfg['sudo'] = True cfg['sudo'] = True
cfg['dist'] = 'trusty' cfg['dist'] = 'trusty'
cfg['language'] = 'python' cfg['language'] = 'python'
cfg['cache'] = 'pip' cfg['cache'] = {
'pip': True,
'directories': [
'./rust/target/'
]
}
cfg['git'] = { cfg['git'] = {
'submodules': False 'submodules': False
@ -31,7 +36,7 @@ make -e install-$BUILD
cfg['script'] = ["make -e $BUILD"] cfg['script'] = ["make -e $BUILD"]
matrix = [] matrix = []
cfg['matrix'] = {'include': matrix} cfg['matrix'] = {'include': matrix, 'fast_finish': True}
matrix.append({ matrix.append({
'python': latest_python, 'python': latest_python,

View file

@ -8,3 +8,6 @@ if [ "$TRAVIS_OS_NAME" = "osx" ]; then
virtualenv -p python3 $HOME/osx-py3 virtualenv -p python3 $HOME/osx-py3
. $HOME/osx-py3/bin/activate . $HOME/osx-py3/bin/activate
fi fi
make install-rust
export PATH="$HOME/.cargo/bin/:$PATH"

View file

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

View file

@ -9,6 +9,7 @@ how to package vdirsyncer.
from setuptools import Command, find_packages, setup from setuptools import Command, find_packages, setup
milksnake = 'milksnake'
requirements = [ requirements = [
# https://github.com/mitsuhiko/click/issues/200 # https://github.com/mitsuhiko/click/issues/200
@ -32,10 +33,29 @@ requirements = [
'requests_toolbelt >=0.4.0', 'requests_toolbelt >=0.4.0',
# https://github.com/untitaker/python-atomicwrites/commit/4d12f23227b6a944ab1d99c507a69fdbc7c9ed6d # noqa # https://github.com/untitaker/python-atomicwrites/commit/4d12f23227b6a944ab1d99c507a69fdbc7c9ed6d # noqa
'atomicwrites>=0.1.7' 'atomicwrites>=0.1.7',
milksnake
] ]
def build_native(spec):
build = spec.add_external_build(
cmd=['make', 'rust-ext'],
path='.'
)
spec.add_cffi_module(
module_path='vdirsyncer._native',
dylib=lambda: build.find_dylib(
'vdirsyncer_rustext', in_path='rust/target/release'),
header_filename=lambda: build.find_header(
'vdirsyncer_rustext.h', in_path='rust/target'),
# Rust bug: If thread-local storage is used, this flag is necessary
# (mitsuhiko)
rtld_flags=['NODELETE']
)
class PrintRequirements(Command): class PrintRequirements(Command):
description = 'Prints minimal requirements' description = 'Prints minimal requirements'
user_options = [] user_options = []
@ -75,7 +95,10 @@ setup(
}, },
# Build dependencies # Build dependencies
setup_requires=['setuptools_scm != 1.12.0'], setup_requires=[
'setuptools_scm != 1.12.0',
milksnake,
],
# Other # Other
packages=find_packages(exclude=['tests.*', 'tests']), packages=find_packages(exclude=['tests.*', 'tests']),
@ -101,4 +124,7 @@ setup(
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Utilities', 'Topic :: Utilities',
], ],
milksnake_tasks=[build_native],
zip_safe=False,
platforms='any'
) )

View file

@ -3,9 +3,11 @@
Test suite for vdirsyncer. Test suite for vdirsyncer.
''' '''
import random
import hypothesis.strategies as st import hypothesis.strategies as st
from vdirsyncer.vobject import normalize_item from vdirsyncer.vobject import Item
import urllib3 import urllib3
import urllib3.exceptions import urllib3.exceptions
@ -18,7 +20,7 @@ def blow_up(*a, **kw):
def assert_item_equals(a, b): def assert_item_equals(a, b):
assert normalize_item(a) == normalize_item(b) assert a.hash == b.hash
VCARD_TEMPLATE = u'''BEGIN:VCARD VCARD_TEMPLATE = u'''BEGIN:VCARD
@ -109,3 +111,10 @@ uid_strategy = st.text(
)), )),
min_size=1 min_size=1
).filter(lambda x: x.strip() == x) ).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,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import random
import uuid import uuid
import textwrap import textwrap
@ -16,7 +15,8 @@ from vdirsyncer.storage.base import normalize_meta_value
from vdirsyncer.vobject import Item from vdirsyncer.vobject import Item
from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \ from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
assert_item_equals, normalize_item, printable_characters_strategy assert_item_equals, format_item, \
printable_characters_strategy
def get_server_mixin(server_name): def get_server_mixin(server_name):
@ -25,12 +25,6 @@ def get_server_mixin(server_name):
return x.ServerMixin return x.ServerMixin
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(object): class StorageTests(object):
storage_class = None storage_class = None
supports_collections = True supports_collections = True
@ -62,7 +56,7 @@ class StorageTests(object):
'VCARD': VCARD_TEMPLATE, 'VCARD': VCARD_TEMPLATE,
}[item_type] }[item_type]
return lambda **kw: format_item(template, **kw) return lambda **kw: format_item(item_template=template, **kw)
@pytest.fixture @pytest.fixture
def requires_collections(self): def requires_collections(self):
@ -299,7 +293,7 @@ class StorageTests(object):
@given(value=st.one_of( @given(value=st.one_of(
st.none(), st.none(),
printable_characters_strategy.filter(lambda x: x.strip() != x) printable_characters_strategy
)) ))
def test_metadata_normalization(self, requires_metadata, s, value): def test_metadata_normalization(self, requires_metadata, s, value):
x = s.get_meta('displayname') x = s.get_meta('displayname')
@ -354,4 +348,5 @@ class StorageTests(object):
href, etag = s.upload(item) href, etag = s.upload(item)
item2, etag2 = s.get(href) item2, etag2 = s.get(href)
assert normalize_item(item) == normalize_item(item2) assert item2.raw.count('BEGIN:VEVENT') == 2
assert 'RRULE' in item2.raw

View file

@ -6,14 +6,8 @@ import os
import pytest import pytest
import requests
import requests.exceptions
from tests import assert_item_equals from tests import assert_item_equals
from vdirsyncer import exceptions
from vdirsyncer.vobject import Item
from .. import StorageTests, get_server_mixin from .. import StorageTests, get_server_mixin
@ -24,14 +18,6 @@ ServerMixin = get_server_mixin(dav_server)
class DAVStorageTests(ServerMixin, StorageTests): class DAVStorageTests(ServerMixin, StorageTests):
dav_server = dav_server dav_server = dav_server
@pytest.mark.skipif(dav_server == 'radicale',
reason='Radicale is very tolerant.')
def test_dav_broken_item(self, s):
item = Item(u'HAHA:YES')
with pytest.raises((exceptions.Error, requests.exceptions.HTTPError)):
s.upload(item)
assert not list(s.list())
def test_dav_empty_get_multi_performance(self, s, monkeypatch): def test_dav_empty_get_multi_performance(self, s, monkeypatch):
def breakdown(*a, **kw): def breakdown(*a, **kw):
raise AssertionError('Expected not to be called.') raise AssertionError('Expected not to be called.')

View file

@ -28,7 +28,7 @@ class TestCalDAVStorage(DAVStorageTests):
s = self.storage_class(item_types=(item_type,), **get_storage_args()) s = self.storage_class(item_types=(item_type,), **get_storage_args())
try: try:
s.upload(format_item(VCARD_TEMPLATE)) s.upload(format_item(item_template=VCARD_TEMPLATE))
except (exceptions.Error, requests.exceptions.HTTPError): except (exceptions.Error, requests.exceptions.HTTPError):
pass pass
assert not list(s.list()) assert not list(s.list())
@ -64,7 +64,7 @@ class TestCalDAVStorage(DAVStorageTests):
s = self.storage_class(start_date=start_date, end_date=end_date, s = self.storage_class(start_date=start_date, end_date=end_date,
**get_storage_args()) **get_storage_args())
too_old_item = format_item(dedent(u''' too_old_item = format_item(item_template=dedent(u'''
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -78,7 +78,7 @@ class TestCalDAVStorage(DAVStorageTests):
END:VCALENDAR END:VCALENDAR
''').strip()) ''').strip())
too_new_item = format_item(dedent(u''' too_new_item = format_item(item_template=dedent(u'''
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -92,7 +92,7 @@ class TestCalDAVStorage(DAVStorageTests):
END:VCALENDAR END:VCALENDAR
''').strip()) ''').strip())
good_item = format_item(dedent(u''' good_item = format_item(item_template=dedent(u'''
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -136,8 +136,8 @@ class TestCalDAVStorage(DAVStorageTests):
@pytest.mark.skipif(dav_server == 'icloud', @pytest.mark.skipif(dav_server == 'icloud',
reason='iCloud only accepts VEVENT') reason='iCloud only accepts VEVENT')
def test_item_types_general(self, s): def test_item_types_general(self, s):
event = s.upload(format_item(EVENT_TEMPLATE))[0] event = s.upload(format_item(item_template=EVENT_TEMPLATE))[0]
task = s.upload(format_item(TASK_TEMPLATE))[0] task = s.upload(format_item(item_template=TASK_TEMPLATE))[0]
s.item_types = ('VTODO', 'VEVENT') s.item_types = ('VTODO', 'VEVENT')
def l(): def l():

@ -1 +1 @@
Subproject commit a27144ddcf39a3283179a4f7ce1ab22b2e810205 Subproject commit 1427b0e8d6cf89c5876bce2dece3248822a2c147

@ -1 +1 @@
Subproject commit bb4fcc6f524467d58c95f1dcec8470fdfcd65adf Subproject commit d3cfc453b0b2dded90c870fd23281fb0c13d766f

View file

@ -5,9 +5,9 @@ import subprocess
import pytest import pytest
from vdirsyncer.storage.filesystem import FilesystemStorage from vdirsyncer.storage.filesystem import FilesystemStorage
from vdirsyncer.vobject import Item
from . import StorageTests from . import StorageTests
from tests import format_item
class TestFilesystemStorage(StorageTests): class TestFilesystemStorage(StorageTests):
@ -42,13 +42,13 @@ class TestFilesystemStorage(StorageTests):
def test_ident_with_slash(self, tmpdir): def test_ident_with_slash(self, tmpdir):
s = self.storage_class(str(tmpdir), '.txt') s = self.storage_class(str(tmpdir), '.txt')
s.upload(Item(u'UID:a/b/c')) s.upload(format_item('a/b/c'))
item_file, = tmpdir.listdir() item_file, = tmpdir.listdir()
assert '/' not in item_file.basename and item_file.isfile() assert '/' not in item_file.basename and item_file.isfile()
def test_too_long_uid(self, tmpdir): def test_too_long_uid(self, tmpdir):
s = self.storage_class(str(tmpdir), '.txt') s = self.storage_class(str(tmpdir), '.txt')
item = Item(u'UID:' + u'hue' * 600) item = format_item('hue' * 600)
href, etag = s.upload(item) href, etag = s.upload(item)
assert item.uid not in href assert item.uid not in href
@ -60,7 +60,7 @@ class TestFilesystemStorage(StorageTests):
monkeypatch.setattr(subprocess, 'call', check_call_mock) monkeypatch.setattr(subprocess, 'call', check_call_mock)
s = self.storage_class(str(tmpdir), '.txt', post_hook=None) s = self.storage_class(str(tmpdir), '.txt', post_hook=None)
s.upload(Item(u'UID:a/b/c')) s.upload(format_item('a/b/c'))
def test_post_hook_active(self, tmpdir, monkeypatch): def test_post_hook_active(self, tmpdir, monkeypatch):
@ -75,7 +75,7 @@ class TestFilesystemStorage(StorageTests):
monkeypatch.setattr(subprocess, 'call', check_call_mock) monkeypatch.setattr(subprocess, 'call', check_call_mock)
s = self.storage_class(str(tmpdir), '.txt', post_hook=exe) s = self.storage_class(str(tmpdir), '.txt', post_hook=exe)
s.upload(Item(u'UID:a/b/c')) s.upload(format_item('a/b/c'))
assert calls assert calls
def test_ignore_git_dirs(self, tmpdir): def test_ignore_git_dirs(self, tmpdir):

View file

@ -4,10 +4,9 @@ import pytest
from requests import Response from requests import Response
from tests import normalize_item
from vdirsyncer.exceptions import UserError from vdirsyncer.exceptions import UserError
from vdirsyncer.storage.http import HttpStorage, prepare_auth from vdirsyncer.storage.http import HttpStorage, prepare_auth
from vdirsyncer.vobject import Item
def test_list(monkeypatch): def test_list(monkeypatch):
@ -56,9 +55,9 @@ def test_list(monkeypatch):
item, etag2 = s.get(href) item, etag2 = s.get(href)
assert item.uid is not None assert item.uid is not None
assert etag2 == etag assert etag2 == etag
found_items[normalize_item(item)] = href found_items[item.hash] = href
expected = set(normalize_item(u'BEGIN:VCALENDAR\n' + x + '\nEND:VCALENDAR') expected = set(Item(u'BEGIN:VCALENDAR\n' + x + '\nEND:VCALENDAR').hash
for x in items) for x in items)
assert set(found_items) == expected assert set(found_items) == expected
@ -67,7 +66,7 @@ def test_list(monkeypatch):
item, etag2 = s.get(href) item, etag2 = s.get(href)
assert item.uid is not None assert item.uid is not None
assert etag2 == etag assert etag2 == etag
assert found_items[normalize_item(item)] == href assert found_items[item.hash] == href
def test_readonly_param(): def test_readonly_param():

View file

@ -62,7 +62,7 @@ def test_repair_uids(storage, runner, repair_uids):
assert 'UID or href is unsafe, assigning random UID' in result.output assert 'UID or href is unsafe, assigning random UID' in result.output
assert not f.exists() assert not f.exists()
new_f, = storage.listdir() new_f, = storage.listdir()
s = new_f.read() s = new_f.read().strip()
assert s.startswith('BEGIN:VCARD') assert s.startswith('BEGIN:VCARD')
assert s.endswith('END:VCARD') assert s.endswith('END:VCARD')

View file

@ -9,6 +9,8 @@ from hypothesis import example, given
import pytest import pytest
from tests import format_item
def test_simple_run(tmpdir, runner): def test_simple_run(tmpdir, runner):
runner.write_with_general(dedent(''' runner.write_with_general(dedent('''
@ -37,10 +39,11 @@ def test_simple_run(tmpdir, runner):
result = runner.invoke(['sync']) result = runner.invoke(['sync'])
assert not result.exception assert not result.exception
tmpdir.join('path_a/haha.txt').write('UID:haha') item = format_item('haha')
tmpdir.join('path_a/haha.txt').write(item.raw)
result = runner.invoke(['sync']) result = runner.invoke(['sync'])
assert 'Copying (uploading) item haha to my_b' in result.output assert 'Copying (uploading) item haha to my_b' in result.output
assert tmpdir.join('path_b/haha.txt').read() == 'UID:haha' assert tmpdir.join('path_b/haha.txt').read() == item.raw
def test_sync_inexistant_pair(tmpdir, runner): def test_sync_inexistant_pair(tmpdir, runner):
@ -109,7 +112,8 @@ def test_empty_storage(tmpdir, runner):
result = runner.invoke(['sync']) result = runner.invoke(['sync'])
assert not result.exception assert not result.exception
tmpdir.join('path_a/haha.txt').write('UID:haha') item = format_item('haha')
tmpdir.join('path_a/haha.txt').write(item.raw)
result = runner.invoke(['sync']) result = runner.invoke(['sync'])
assert not result.exception assert not result.exception
tmpdir.join('path_b/haha.txt').remove() tmpdir.join('path_b/haha.txt').remove()
@ -152,7 +156,7 @@ def test_collections_cache_invalidation(tmpdir, runner):
collections = ["a", "b", "c"] collections = ["a", "b", "c"]
''').format(str(tmpdir))) ''').format(str(tmpdir)))
foo.join('a/itemone.txt').write('UID:itemone') foo.join('a/itemone.txt').write(format_item('itemone').raw)
result = runner.invoke(['discover']) result = runner.invoke(['discover'])
assert not result.exception assert not result.exception
@ -347,9 +351,10 @@ def test_ident_conflict(tmpdir, runner):
foo = tmpdir.mkdir('foo') foo = tmpdir.mkdir('foo')
tmpdir.mkdir('bar') tmpdir.mkdir('bar')
foo.join('one.txt').write('UID:1') item = format_item('1')
foo.join('two.txt').write('UID:1') foo.join('one.txt').write(item.raw)
foo.join('three.txt').write('UID:1') foo.join('two.txt').write(item.raw)
foo.join('three.txt').write(item.raw)
result = runner.invoke(['discover']) result = runner.invoke(['discover'])
assert not result.exception assert not result.exception
@ -403,8 +408,12 @@ def test_no_configured_pairs(tmpdir, runner, cmd):
assert result.exception.code == 5 assert result.exception.code == 5
item_a = format_item('lol')
item_b = format_item('lol')
@pytest.mark.parametrize('resolution,expect_foo,expect_bar', [ @pytest.mark.parametrize('resolution,expect_foo,expect_bar', [
(['command', 'cp'], 'UID:lol\nfööcontent', 'UID:lol\nfööcontent') (['command', 'cp'], item_a.raw, item_a.raw)
]) ])
def test_conflict_resolution(tmpdir, runner, resolution, expect_foo, def test_conflict_resolution(tmpdir, runner, resolution, expect_foo,
expect_bar): expect_bar):
@ -429,9 +438,9 @@ def test_conflict_resolution(tmpdir, runner, resolution, expect_foo,
foo = tmpdir.join('foo') foo = tmpdir.join('foo')
bar = tmpdir.join('bar') bar = tmpdir.join('bar')
fooitem = foo.join('lol.txt').ensure() fooitem = foo.join('lol.txt').ensure()
fooitem.write('UID:lol\nfööcontent') fooitem.write(item_a.raw)
baritem = bar.join('lol.txt').ensure() baritem = bar.join('lol.txt').ensure()
baritem.write('UID:lol\nbööcontent') baritem.write(item_b.raw)
r = runner.invoke(['discover']) r = runner.invoke(['discover'])
assert not r.exception assert not r.exception
@ -471,11 +480,12 @@ def test_partial_sync(tmpdir, runner, partial_sync):
foo = tmpdir.mkdir('foo') foo = tmpdir.mkdir('foo')
bar = tmpdir.mkdir('bar') bar = tmpdir.mkdir('bar')
foo.join('other.txt').write('UID:other') item = format_item('other')
bar.join('other.txt').write('UID:other') foo.join('other.txt').write(item.raw)
bar.join('other.txt').write(item.raw)
baritem = bar.join('lol.txt') baritem = bar.join('lol.txt')
baritem.write('UID:lol') baritem.write(format_item('lol').raw)
r = runner.invoke(['discover']) r = runner.invoke(['discover'])
assert not r.exception assert not r.exception

View file

@ -8,7 +8,7 @@ from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
import pytest import pytest
from tests import blow_up, uid_strategy from tests import blow_up, format_item, uid_strategy
from vdirsyncer.storage.memory import MemoryStorage, _random_string from vdirsyncer.storage.memory import MemoryStorage, _random_string
from vdirsyncer.sync import sync as _sync from vdirsyncer.sync import sync as _sync
@ -49,7 +49,7 @@ def test_missing_status():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
item = Item(u'asdf') item = format_item('asdf')
a.upload(item) a.upload(item)
b.upload(item) b.upload(item)
sync(a, b, status) sync(a, b, status)
@ -62,8 +62,8 @@ def test_missing_status_and_different_items():
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
item1 = Item(u'UID:1\nhaha') item1 = format_item('1')
item2 = Item(u'UID:1\nhoho') item2 = format_item('1')
a.upload(item1) a.upload(item1)
b.upload(item2) b.upload(item2)
with pytest.raises(SyncConflict): with pytest.raises(SyncConflict):
@ -79,8 +79,8 @@ def test_read_only_and_prefetch():
b.read_only = True b.read_only = True
status = {} status = {}
item1 = Item(u'UID:1\nhaha') item1 = format_item('1')
item2 = Item(u'UID:2\nhoho') item2 = format_item('2')
a.upload(item1) a.upload(item1)
a.upload(item2) a.upload(item2)
@ -95,7 +95,8 @@ def test_partial_sync_error():
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
a.upload(Item('UID:0')) item = format_item('0')
a.upload(item)
b.read_only = True b.read_only = True
with pytest.raises(PartialSync): with pytest.raises(PartialSync):
@ -107,13 +108,13 @@ def test_partial_sync_ignore():
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
item0 = Item('UID:0\nhehe') item0 = format_item('0')
a.upload(item0) a.upload(item0)
b.upload(item0) b.upload(item0)
b.read_only = True b.read_only = True
item1 = Item('UID:1\nhaha') item1 = format_item('1')
a.upload(item1) a.upload(item1)
sync(a, b, status, partial_sync='ignore') sync(a, b, status, partial_sync='ignore')
@ -128,23 +129,25 @@ def test_partial_sync_ignore2():
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href, etag = a.upload(Item('UID:0')) item = format_item('0')
href, etag = a.upload(item)
a.read_only = True a.read_only = True
sync(a, b, status, partial_sync='ignore', force_delete=True) sync(a, b, status, partial_sync='ignore', force_delete=True)
assert items(b) == items(a) == {'UID:0'} assert items(b) == items(a) == {item.raw}
b.items.clear() b.items.clear()
sync(a, b, status, partial_sync='ignore', force_delete=True) sync(a, b, status, partial_sync='ignore', force_delete=True)
sync(a, b, status, partial_sync='ignore', force_delete=True) sync(a, b, status, partial_sync='ignore', force_delete=True)
assert items(a) == {'UID:0'} assert items(a) == {item.raw}
assert not b.items assert not b.items
a.read_only = False a.read_only = False
a.update(href, Item('UID:0\nupdated'), etag) new_item = format_item('0')
a.update(href, new_item, etag)
a.read_only = True a.read_only = True
sync(a, b, status, partial_sync='ignore', force_delete=True) sync(a, b, status, partial_sync='ignore', force_delete=True)
assert items(b) == items(a) == {'UID:0\nupdated'} assert items(b) == items(a) == {new_item.raw}
def test_upload_and_update(): def test_upload_and_update():
@ -152,22 +155,22 @@ def test_upload_and_update():
b = MemoryStorage(fileext='.b') b = MemoryStorage(fileext='.b')
status = {} status = {}
item = Item(u'UID:1') # new item 1 in a item = format_item('1') # new item 1 in a
a.upload(item) a.upload(item)
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw} assert items(b) == items(a) == {item.raw}
item = Item(u'UID:1\nASDF:YES') # update of item 1 in b item = format_item('1') # update of item 1 in b
b.update('1.b', item, b.get('1.b')[1]) b.update('1.b', item, b.get('1.b')[1])
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw} assert items(b) == items(a) == {item.raw}
item2 = Item(u'UID:2') # new item 2 in b item2 = format_item('2') # new item 2 in b
b.upload(item2) b.upload(item2)
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw, item2.raw} assert items(b) == items(a) == {item.raw, item2.raw}
item2 = Item(u'UID:2\nASDF:YES') # update of item 2 in a item2 = format_item('2') # update of item 2 in a
a.update('2.a', item2, a.get('2.a')[1]) a.update('2.a', item2, a.get('2.a')[1])
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw, item2.raw} assert items(b) == items(a) == {item.raw, item2.raw}
@ -178,9 +181,9 @@ def test_deletion():
b = MemoryStorage(fileext='.b') b = MemoryStorage(fileext='.b')
status = {} status = {}
item = Item(u'UID:1') item = format_item('1')
a.upload(item) a.upload(item)
item2 = Item(u'UID:2') item2 = format_item('2')
a.upload(item2) a.upload(item2)
sync(a, b, status) sync(a, b, status)
b.delete('1.b', b.get('1.b')[1]) b.delete('1.b', b.get('1.b')[1])
@ -200,14 +203,14 @@ def test_insert_hash():
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
item = Item('UID:1') item = format_item('1')
href, etag = a.upload(item) href, etag = a.upload(item)
sync(a, b, status) sync(a, b, status)
for d in status['1']: for d in status['1']:
del d['hash'] del d['hash']
a.update(href, Item('UID:1\nHAHA:YES'), etag) a.update(href, format_item('1'), etag) # new item content
sync(a, b, status) sync(a, b, status)
assert 'hash' in status['1'][0] and 'hash' in status['1'][1] assert 'hash' in status['1'][0] and 'hash' in status['1'][1]
@ -215,7 +218,7 @@ def test_insert_hash():
def test_already_synced(): def test_already_synced():
a = MemoryStorage(fileext='.a') a = MemoryStorage(fileext='.a')
b = MemoryStorage(fileext='.b') b = MemoryStorage(fileext='.b')
item = Item(u'UID:1') item = format_item('1')
a.upload(item) a.upload(item)
b.upload(item) b.upload(item)
status = { status = {
@ -243,14 +246,14 @@ def test_already_synced():
def test_conflict_resolution_both_etags_new(winning_storage): def test_conflict_resolution_both_etags_new(winning_storage):
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item = Item(u'UID:1') item = format_item('1')
href_a, etag_a = a.upload(item) href_a, etag_a = a.upload(item)
href_b, etag_b = b.upload(item) href_b, etag_b = b.upload(item)
status = {} status = {}
sync(a, b, status) sync(a, b, status)
assert status assert status
item_a = Item(u'UID:1\nitem a') item_a = format_item('1')
item_b = Item(u'UID:1\nitem b') item_b = format_item('1')
a.update(href_a, item_a, etag_a) a.update(href_a, item_a, etag_a)
b.update(href_b, item_b, etag_b) b.update(href_b, item_b, etag_b)
with pytest.raises(SyncConflict): with pytest.raises(SyncConflict):
@ -264,13 +267,14 @@ def test_conflict_resolution_both_etags_new(winning_storage):
def test_updated_and_deleted(): def test_updated_and_deleted():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
href_a, etag_a = a.upload(Item(u'UID:1')) item = format_item('1')
href_a, etag_a = a.upload(item)
status = {} status = {}
sync(a, b, status, force_delete=True) sync(a, b, status, force_delete=True)
(href_b, etag_b), = b.list() (href_b, etag_b), = b.list()
b.delete(href_b, etag_b) b.delete(href_b, etag_b)
updated = Item(u'UID:1\nupdated') updated = format_item('1')
a.update(href_a, updated, etag_a) a.update(href_a, updated, etag_a)
sync(a, b, status, force_delete=True) sync(a, b, status, force_delete=True)
@ -280,8 +284,8 @@ def test_updated_and_deleted():
def test_conflict_resolution_invalid_mode(): def test_conflict_resolution_invalid_mode():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item_a = Item(u'UID:1\nitem a') item_a = format_item('1')
item_b = Item(u'UID:1\nitem b') item_b = format_item('1')
a.upload(item_a) a.upload(item_a)
b.upload(item_b) b.upload(item_b)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -291,7 +295,7 @@ def test_conflict_resolution_invalid_mode():
def test_conflict_resolution_new_etags_without_changes(): def test_conflict_resolution_new_etags_without_changes():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item = Item(u'UID:1') item = format_item('1')
href_a, etag_a = a.upload(item) href_a, etag_a = a.upload(item)
href_b, etag_b = b.upload(item) href_b, etag_b = b.upload(item)
status = {'1': (href_a, 'BOGUS_a', href_b, 'BOGUS_b')} status = {'1': (href_a, 'BOGUS_a', href_b, 'BOGUS_b')}
@ -326,7 +330,7 @@ def test_uses_get_multi(monkeypatch):
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item = Item(u'UID:1') item = format_item('1')
expected_href, etag = a.upload(item) expected_href, etag = a.upload(item)
sync(a, b, {}) sync(a, b, {})
@ -336,8 +340,8 @@ def test_uses_get_multi(monkeypatch):
def test_empty_storage_dataloss(): def test_empty_storage_dataloss():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
a.upload(Item(u'UID:1')) for i in '12':
a.upload(Item(u'UID:2')) a.upload(format_item(i))
status = {} status = {}
sync(a, b, status) sync(a, b, status)
with pytest.raises(StorageEmpty): with pytest.raises(StorageEmpty):
@ -350,22 +354,24 @@ def test_empty_storage_dataloss():
def test_no_uids(): def test_no_uids():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
a.upload(Item(u'ASDF')) item_a = format_item('')
b.upload(Item(u'FOOBAR')) item_b = format_item('')
a.upload(item_a)
b.upload(item_b)
status = {} status = {}
sync(a, b, status) sync(a, b, status)
assert items(a) == items(b) == {u'ASDF', u'FOOBAR'} assert items(a) == items(b) == {item_a.raw, item_b.raw}
def test_changed_uids(): def test_changed_uids():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
href_a, etag_a = a.upload(Item(u'UID:A-ONE')) href_a, etag_a = a.upload(format_item('a1'))
href_b, etag_b = b.upload(Item(u'UID:B-ONE')) href_b, etag_b = b.upload(format_item('b1'))
status = {} status = {}
sync(a, b, status) sync(a, b, status)
a.update(href_a, Item(u'UID:A-TWO'), etag_a) a.update(href_a, format_item('a2'), etag_a)
sync(a, b, status) sync(a, b, status)
@ -383,34 +389,37 @@ def test_partial_sync_revert():
a = MemoryStorage(instance_name='a') a = MemoryStorage(instance_name='a')
b = MemoryStorage(instance_name='b') b = MemoryStorage(instance_name='b')
status = {} status = {}
a.upload(Item(u'UID:1')) item1 = format_item('1')
b.upload(Item(u'UID:2')) item2 = format_item('2')
a.upload(item1)
b.upload(item2)
b.read_only = True b.read_only = True
sync(a, b, status, partial_sync='revert') sync(a, b, status, partial_sync='revert')
assert len(status) == 2 assert len(status) == 2
assert items(a) == {'UID:1', 'UID:2'} assert items(a) == {item1.raw, item2.raw}
assert items(b) == {'UID:2'} assert items(b) == {item2.raw}
sync(a, b, status, partial_sync='revert') sync(a, b, status, partial_sync='revert')
assert len(status) == 1 assert len(status) == 1
assert items(a) == {'UID:2'} assert items(a) == {item2.raw}
assert items(b) == {'UID:2'} assert items(b) == {item2.raw}
# Check that updates get reverted # Check that updates get reverted
a.items[next(iter(a.items))] = ('foo', Item('UID:2\nupdated')) item2_up = format_item('2')
assert items(a) == {'UID:2\nupdated'} a.items[next(iter(a.items))] = ('foo', item2_up)
assert items(a) == {item2_up.raw}
sync(a, b, status, partial_sync='revert') sync(a, b, status, partial_sync='revert')
assert len(status) == 1 assert len(status) == 1
assert items(a) == {'UID:2\nupdated'} assert items(a) == {item2_up.raw}
sync(a, b, status, partial_sync='revert') sync(a, b, status, partial_sync='revert')
assert items(a) == {'UID:2'} assert items(a) == {item2.raw}
# Check that deletions get reverted # Check that deletions get reverted
a.items.clear() a.items.clear()
sync(a, b, status, partial_sync='revert', force_delete=True) sync(a, b, status, partial_sync='revert', force_delete=True)
sync(a, b, status, partial_sync='revert', force_delete=True) sync(a, b, status, partial_sync='revert', force_delete=True)
assert items(a) == {'UID:2'} assert items(a) == {item2.raw}
@pytest.mark.parametrize('sync_inbetween', (True, False)) @pytest.mark.parametrize('sync_inbetween', (True, False))
@ -418,13 +427,16 @@ def test_ident_conflict(sync_inbetween):
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href_a, etag_a = a.upload(Item(u'UID:aaa')) item_a = format_item('aaa')
href_b, etag_b = a.upload(Item(u'UID:bbb')) item_b = format_item('bbb')
href_a, etag_a = a.upload(item_a)
href_b, etag_b = a.upload(item_b)
if sync_inbetween: if sync_inbetween:
sync(a, b, status) sync(a, b, status)
a.update(href_a, Item(u'UID:xxx'), etag_a) item_x = format_item('xxx')
a.update(href_b, Item(u'UID:xxx'), etag_b) a.update(href_a, item_x, etag_a)
a.update(href_b, item_x, etag_b)
with pytest.raises(IdentConflict): with pytest.raises(IdentConflict):
sync(a, b, status) sync(a, b, status)
@ -441,7 +453,8 @@ def test_moved_href():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href, etag = a.upload(Item(u'UID:haha')) item = format_item('haha')
href, etag = a.upload(item)
sync(a, b, status) sync(a, b, status)
b.items['lol'] = b.items.pop('haha') b.items['lol'] = b.items.pop('haha')
@ -454,7 +467,7 @@ def test_moved_href():
sync(a, b, status) sync(a, b, status)
assert len(status) == 1 assert len(status) == 1
assert items(a) == items(b) == {'UID:haha'} assert items(a) == items(b) == {item.raw}
assert status['haha'][1]['href'] == 'lol' assert status['haha'][1]['href'] == 'lol'
old_status = deepcopy(status) old_status = deepcopy(status)
@ -463,7 +476,7 @@ def test_moved_href():
sync(a, b, status) sync(a, b, status)
assert old_status == status assert old_status == status
assert items(a) == items(b) == {'UID:haha'} assert items(a) == items(b) == {item.raw}
def test_bogus_etag_change(): def test_bogus_etag_change():
@ -476,26 +489,31 @@ def test_bogus_etag_change():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href_a, etag_a = a.upload(Item(u'UID:ASDASD')) item = format_item('ASDASD')
sync(a, b, status)
assert len(status) == len(list(a.list())) == len(list(b.list())) == 1
href_a, etag_a = a.upload(item)
sync(a, b, status)
assert len(status) == 1
assert items(a) == items(b) == {item.raw}
new_item = format_item('ASDASD')
(href_b, etag_b), = b.list() (href_b, etag_b), = b.list()
a.update(href_a, Item(u'UID:ASDASD'), etag_a) a.update(href_a, item, etag_a)
b.update(href_b, Item(u'UID:ASDASD\nACTUALCHANGE:YES'), etag_b) b.update(href_b, new_item, etag_b)
b.delete = b.update = b.upload = blow_up b.delete = b.update = b.upload = blow_up
sync(a, b, status) sync(a, b, status)
assert len(status) == 1 assert len(status) == 1
assert items(a) == items(b) == {u'UID:ASDASD\nACTUALCHANGE:YES'} assert items(a) == items(b) == {new_item.raw}
def test_unicode_hrefs(): def test_unicode_hrefs():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href, etag = a.upload(Item(u'UID:äää')) item = format_item('äää')
href, etag = a.upload(item)
sync(a, b, status) sync(a, b, status)
@ -565,7 +583,7 @@ class SyncMachine(RuleBasedStateMachine):
uid=uid_strategy, uid=uid_strategy,
etag=st.text()) etag=st.text())
def upload(self, storage, uid, etag): def upload(self, storage, uid, etag):
item = Item(u'UID:{}'.format(uid)) item = Item('BEGIN:VCARD\r\nUID:{}\r\nEND:VCARD'.format(uid))
storage.items[uid] = (etag, item) storage.items[uid] = (etag, item)
@rule(storage=Storage, href=st.text()) @rule(storage=Storage, href=st.text())
@ -643,8 +661,8 @@ def test_rollback(error_callback):
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
a.items['0'] = ('', Item('UID:0')) a.items['0'] = ('', format_item('0'))
b.items['1'] = ('', Item('UID:1')) b.items['1'] = ('', format_item('1'))
b.upload = b.update = b.delete = action_failure b.upload = b.update = b.delete = action_failure
@ -668,7 +686,7 @@ def test_duplicate_hrefs():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
a.list = lambda: [('a', 'a')] * 3 a.list = lambda: [('a', 'a')] * 3
a.items['a'] = ('a', Item('UID:a')) a.items['a'] = ('a', format_item('a'))
status = {} status = {}
sync(a, b, status) sync(a, b, status)

View file

@ -38,7 +38,7 @@ def test_repair_uids(uid):
@settings(perform_health_check=False) # Using the random module for UIDs @settings(perform_health_check=False) # Using the random module for UIDs
def test_repair_unsafe_uids(uid): def test_repair_unsafe_uids(uid):
s = MemoryStorage() s = MemoryStorage()
item = Item(u'BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid)) item = Item(u'BEGIN:VCARD\nUID:123\nEND:VCARD').with_uid(uid)
href, etag = s.upload(item) href, etag = s.upload(item)
assert s.get(href)[0].uid == uid assert s.get(href)[0].uid == uid
assert not href_safe(uid) assert not href_safe(uid)

View file

@ -9,7 +9,7 @@ from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
import pytest import pytest
from tests import BARE_EVENT_TEMPLATE, EVENT_TEMPLATE, \ from tests import BARE_EVENT_TEMPLATE, EVENT_TEMPLATE, \
EVENT_WITH_TIMEZONE_TEMPLATE, VCARD_TEMPLATE, normalize_item, \ EVENT_WITH_TIMEZONE_TEMPLATE, VCARD_TEMPLATE, \
uid_strategy uid_strategy
import vdirsyncer.vobject as vobject import vdirsyncer.vobject as vobject
@ -31,8 +31,8 @@ _simple_joined = u'\r\n'.join(
def test_split_collection_simple(benchmark): def test_split_collection_simple(benchmark):
given = benchmark(lambda: list(vobject.split_collection(_simple_joined))) given = benchmark(lambda: list(vobject.split_collection(_simple_joined)))
assert [normalize_item(item) for item in given] == \ assert [vobject.Item(item).hash for item in given] == \
[normalize_item(item) for item in _simple_split] [vobject.Item(item).hash for item in _simple_split]
assert [x.splitlines() for x in given] == \ assert [x.splitlines() for x in given] == \
[x.splitlines() for x in _simple_split] [x.splitlines() for x in _simple_split]
@ -47,8 +47,8 @@ def test_split_collection_multiple_wrappers(benchmark):
) )
given = benchmark(lambda: list(vobject.split_collection(joined))) given = benchmark(lambda: list(vobject.split_collection(joined)))
assert [normalize_item(item) for item in given] == \ assert [vobject.Item(item).hash for item in given] == \
[normalize_item(item) for item in _simple_split] [vobject.Item(item).hash for item in _simple_split]
assert [x.splitlines() for x in given] == \ assert [x.splitlines() for x in given] == \
[x.splitlines() for x in _simple_split] [x.splitlines() for x in _simple_split]
@ -56,7 +56,7 @@ def test_split_collection_multiple_wrappers(benchmark):
def test_join_collection_simple(benchmark): def test_join_collection_simple(benchmark):
given = benchmark(lambda: vobject.join_collection(_simple_split)) given = benchmark(lambda: vobject.join_collection(_simple_split))
assert normalize_item(given) == normalize_item(_simple_joined) assert vobject.Item(given).hash == vobject.Item(_simple_joined).hash
assert given.splitlines() == _simple_joined.splitlines() assert given.splitlines() == _simple_joined.splitlines()
@ -123,12 +123,12 @@ def test_split_collection_timezones():
[timezone, u'END:VCALENDAR'] [timezone, u'END:VCALENDAR']
) )
given = set(normalize_item(item) given = set(vobject.Item(item).hash
for item in vobject.split_collection(full)) for item in vobject.split_collection(full))
expected = set( expected = set(
normalize_item(u'\r\n'.join(( vobject.Item(u'\r\n'.join((
u'BEGIN:VCALENDAR', item, timezone, u'END:VCALENDAR' u'BEGIN:VCALENDAR', item, timezone, u'END:VCALENDAR'
))) ))).hash
for item in items for item in items
) )
@ -146,11 +146,11 @@ def test_split_contacts():
with_wrapper.splitlines() with_wrapper.splitlines()
def test_hash_item(): def test_hash_item2():
a = EVENT_TEMPLATE.format(r=1, uid=1) a = EVENT_TEMPLATE.format(r=1, uid=1)
b = u'\n'.join(line for line in a.splitlines() b = u'\n'.join(line for line in a.splitlines()
if u'PRODID' not in line) if u'PRODID' not in line)
assert vobject.hash_item(a) == vobject.hash_item(b) assert vobject.Item(a).hash == vobject.Item(b).hash
def test_multiline_uid(benchmark): def test_multiline_uid(benchmark):
@ -223,7 +223,7 @@ def test_replace_uid(template, uid):
item = vobject.Item(template.format(r=123, uid=123)).with_uid(uid) item = vobject.Item(template.format(r=123, uid=123)).with_uid(uid)
assert item.uid == uid assert item.uid == uid
if uid: if uid:
assert item.raw.count('\nUID:{}'.format(uid)) == 1 assert item.raw.count('\nUID:') == 1
else: else:
assert '\nUID:' not in item.raw assert '\nUID:' not in item.raw
@ -351,3 +351,47 @@ def test_component_contains():
with pytest.raises(ValueError): with pytest.raises(ValueError):
42 in item 42 in item
def test_hash_item():
item1 = vobject.Item(
'BEGIN:FOO\r\n'
'X-RADICALE-NAME:YES\r\n'
'END:FOO\r\n'
)
item2 = vobject.Item(
'BEGIN:FOO\r\n'
'X-RADICALE-NAME:NO\r\n'
'END:FOO\r\n'
)
assert item1.hash == item2.hash
item2 = vobject.Item(
'BEGIN:FOO\r\n'
'X-RADICALE-NAME:NO\r\n'
'OTHER-PROP:YAY\r\n'
'END:FOO\r\n'
)
assert item1.hash != item2.hash
def test_hash_item_timezones():
item1 = vobject.Item(
'BEGIN:VCALENDAR\r\n'
'HELLO:HAHA\r\n'
'BEGIN:VTIMEZONE\r\n'
'PROP:YES\r\n'
'END:VTIMEZONE\r\n'
'END:VCALENDAR\r\n'
)
item2 = vobject.Item(
'BEGIN:VCALENDAR\r\n'
'HELLO:HAHA\r\n'
'END:VCALENDAR\r\n'
)
assert item1.hash == item2.hash

View file

@ -79,3 +79,7 @@ class UnsupportedMetadataError(Error, NotImplementedError):
class CollectionRequired(Error): class CollectionRequired(Error):
'''`collection = null` is not allowed.''' '''`collection = null` is not allowed.'''
class VobjectParseError(Error, ValueError):
'''The parsed vobject is invalid.'''

45
vdirsyncer/native.py Normal file
View file

@ -0,0 +1,45 @@
from ._native import ffi, lib
from .exceptions import VobjectParseError
def parse_component(raw):
e = ffi.new('VdirsyncerError *')
try:
c = lib.vdirsyncer_parse_component(raw, e)
if e.failed:
raise VobjectParseError(ffi.string(e.msg).decode('utf-8'))
return _component_rv(c)
finally:
if e.failed:
lib.vdirsyncer_clear_err(e)
def write_component(component):
return _string_rv(lib.vdirsyncer_write_component(component))
def get_uid(component):
return _string_rv(lib.vdirsyncer_get_uid(component))
def _string_rv(c_str):
try:
return ffi.string(c_str).decode('utf-8')
finally:
lib.vdirsyncer_free_str(c_str)
def change_uid(component, uid):
lib.vdirsyncer_change_uid(component, uid.encode('utf-8'))
def _component_rv(c):
return ffi.gc(c, lib.vdirsyncer_free_component)
def clone_component(c):
return _component_rv(lib.vdirsyncer_clone_component(c))
def hash_component(c):
return _string_rv(lib.vdirsyncer_hash_component(c))

View file

@ -146,7 +146,7 @@ class Discover(object):
_homeset_xml = None _homeset_xml = None
_homeset_tag = None _homeset_tag = None
_well_known_uri = None _well_known_uri = None
_collection_xml = b""" _collection_xml = b"""<?xml version="1.0" encoding="utf-8" ?>
<d:propfind xmlns:d="DAV:"> <d:propfind xmlns:d="DAV:">
<d:prop> <d:prop>
<d:resourcetype /> <d:resourcetype />
@ -724,8 +724,8 @@ class CalDAVStorage(DAVStorage):
example, the following would synchronize the timerange from one year in the example, the following would synchronize the timerange from one year in the
past to one year in the future:: past to one year in the future::
start_date = datetime.now() - timedelta(days=365) start_date = "datetime.now() - timedelta(days=365)"
end_date = datetime.now() + timedelta(days=365) end_date = "datetime.now() + timedelta(days=365)"
Either both or none have to be specified. The default is to synchronize Either both or none have to be specified. The default is to synchronize
everything. everything.
@ -792,32 +792,28 @@ class CalDAVStorage(DAVStorage):
@staticmethod @staticmethod
def _get_list_filters(components, start, end): def _get_list_filters(components, start, end):
if components: caldavfilter = '''
caldavfilter = ''' <C:comp-filter name="VCALENDAR">
<C:comp-filter name="VCALENDAR"> <C:comp-filter name="{component}">
<C:comp-filter name="{component}"> {timefilter}
{timefilter}
</C:comp-filter>
</C:comp-filter> </C:comp-filter>
''' </C:comp-filter>
'''
if start is not None and end is not None: timefilter = ''
start = start.strftime(CALDAV_DT_FORMAT)
end = end.strftime(CALDAV_DT_FORMAT)
timefilter = ('<C:time-range start="{start}" end="{end}"/>' if start is not None and end is not None:
.format(start=start, end=end)) start = start.strftime(CALDAV_DT_FORMAT)
else: end = end.strftime(CALDAV_DT_FORMAT)
timefilter = ''
for component in components: timefilter = ('<C:time-range start="{start}" end="{end}"/>'
yield caldavfilter.format(component=component, .format(start=start, end=end))
timefilter=timefilter) if not components:
else: components = ('VTODO', 'VEVENT')
if start is not None and end is not None:
for x in CalDAVStorage._get_list_filters(('VTODO', 'VEVENT'), for component in components:
start, end): yield caldavfilter.format(component=component,
yield x timefilter=timefilter)
def list(self): def list(self):
caldavfilters = list(self._get_list_filters( caldavfilters = list(self._get_list_filters(
@ -833,8 +829,8 @@ class CalDAVStorage(DAVStorage):
# instead? # instead?
# #
# See https://github.com/dmfs/tasks/issues/118 for backstory. # See https://github.com/dmfs/tasks/issues/118 for backstory.
for x in DAVStorage.list(self): yield from DAVStorage.list(self)
yield x return
data = '''<?xml version="1.0" encoding="utf-8" ?> data = '''<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" <C:calendar-query xmlns:D="DAV:"

View file

@ -1,37 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import hashlib
from itertools import chain, tee from itertools import chain, tee
from .utils import cached_property, uniq from .utils import cached_property, uniq
from . import exceptions, native
IGNORE_PROPS = (
# PRODID is changed by radicale for some reason after upload
'PRODID',
# Sometimes METHOD:PUBLISH is added by WebCAL providers, for us it doesn't
# make a difference
'METHOD',
# X-RADICALE-NAME is used by radicale, because hrefs don't really exist in
# their filesystem backend
'X-RADICALE-NAME',
# Apparently this is set by Horde?
# https://github.com/pimutils/vdirsyncer/issues/318
'X-WR-CALNAME',
# Those are from the VCARD specification and is supposed to change when the
# item does -- however, we can determine that ourselves
'REV',
'LAST-MODIFIED',
'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/
'DTSTAMP',
'UID',
)
class Item(object): class Item(object):
@ -39,25 +11,28 @@ class Item(object):
'''Immutable wrapper class for VCALENDAR (VEVENT, VTODO) and '''Immutable wrapper class for VCALENDAR (VEVENT, VTODO) and
VCARD''' VCARD'''
def __init__(self, raw): def __init__(self, raw, component=None):
assert isinstance(raw, str), type(raw) assert isinstance(raw, str), type(raw)
self._raw = raw self._raw = raw
if component is not None:
def with_uid(self, new_uid): self.__dict__['_component'] = component
parsed = _Component.parse(self.raw)
stack = [parsed]
while stack:
component = stack.pop()
stack.extend(component.subcomponents)
if component.name in ('VEVENT', 'VTODO', 'VJOURNAL', 'VCARD'):
del component['UID']
if new_uid:
component['UID'] = new_uid
return Item('\r\n'.join(parsed.dump_lines()))
@cached_property @cached_property
def _component(self):
try:
return native.parse_component(self.raw.encode('utf-8'))
except exceptions.VobjectParseError:
return None
def with_uid(self, new_uid):
if not self._component:
raise ValueError('Item malformed.')
new_c = native.clone_component(self._component)
native.change_uid(new_c, new_uid or '')
return Item(native.write_component(new_c), component=new_c)
@property
def raw(self): def raw(self):
'''Raw content of the item, as unicode string. '''Raw content of the item, as unicode string.
@ -69,18 +44,17 @@ class Item(object):
def uid(self): def uid(self):
'''Global identifier of the item, across storages, doesn't change after '''Global identifier of the item, across storages, doesn't change after
a modification of the item.''' a modification of the item.'''
# Don't actually parse component, but treat all lines as single if not self._component:
# component, avoiding traversal through all subcomponents.
x = _Component('TEMP', self.raw.splitlines(), [])
try:
return x['UID'].strip() or None
except KeyError:
return None return None
return native.get_uid(self._component) or None
@cached_property @cached_property
def hash(self): def hash(self):
'''Hash of self.raw, used for etags.''' '''Used for etags.'''
return hash_item(self.raw) if not self.is_valid:
raise ValueError('Item malformed.')
return native.hash_component(self._component)
@cached_property @cached_property
def ident(self): def ident(self):
@ -99,40 +73,15 @@ class Item(object):
@property @property
def parsed(self): def parsed(self):
'''Don't cache because the rv is mutable.''' '''Don't cache because the rv is mutable.'''
# FIXME: remove
try: try:
return _Component.parse(self.raw) return _Component.parse(self.raw)
except Exception: except Exception:
return None return None
@property
def normalize_item(item, ignore_props=IGNORE_PROPS): def is_valid(self):
'''Create syntactically invalid mess that is equal for similar items.''' return bool(self._component)
if not isinstance(item, Item):
item = Item(item)
item = _strip_timezones(item)
x = _Component('TEMP', item.raw.splitlines(), [])
for prop in IGNORE_PROPS:
del x[prop]
x.props.sort()
return u'\r\n'.join(filter(bool, (line.strip() for line in x.props)))
def _strip_timezones(item):
parsed = item.parsed
if not parsed or parsed.name != 'VCALENDAR':
return item
parsed.subcomponents = [c for c in parsed.subcomponents
if c.name != 'VTIMEZONE']
return Item('\r\n'.join(parsed.dump_lines()))
def hash_item(text):
return hashlib.sha256(normalize_item(text).encode('utf-8')).hexdigest()
def split_collection(text): def split_collection(text):