Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6fdf767
fix(jmap): session lifecycle, tz-correct LocalDateTime, and the revie…
tobixen Jun 19, 2026
c406eb4
chore: server compatibility matrix
tobixen Jun 15, 2026
42cd4c1
test: server-compatibility test suite
tobixen Jun 16, 2026
7479f52
fix: compatibility: comp-type-less search
tobixen Jun 16, 2026
654e1ee
docs(tests): correcting some misinformation
tobixen Jun 4, 2026
f9646d0
fix: compatibility: set calendar canonical URL on create
tobixen Jun 16, 2026
16e0732
fix: config-section handling for get_davclient
tobixen Jun 10, 2026
12e6264
fix: some icalendar handling was incorrect
tobixen Jun 11, 2026
ac0b4ed
fix: eight crash/wrong-result bugs from code review
tobixen Jun 11, 2026
e16b5b6
fix: URL.canonical() __eq__-problems
tobixen Jun 11, 2026
d38e0c4
fix: search.py code-review bugs
tobixen Jun 11, 2026
697e927
fix: Several silent wrong-result bugs
tobixen Jun 11, 2026
491f2b1
fix: require_tls=True not enforced on well-known URI redirect
tobixen Jun 11, 2026
09769fd
fix: XML parser in response.py lacked entity hardening
tobixen Jun 11, 2026
da4e7d5
fix: three more crash bugs from code review
tobixen Jun 11, 2026
aae016e
fix: let PYTHON_CALDAV_COMMDUMP yield warnings
tobixen Jun 18, 2026
bc255e4
fix: code-review edge-case bugs (§1.2–1.5, §2.14, §2.15)
tobixen Jun 11, 2026
dd94cbd
docs: SECURITY notes + code review cleanup
tobixen Jun 13, 2026
81e81fe
fix: replace empty except-blocks with warn/skip logic
tobixen Jun 14, 2026
ed3aa90
perf: use calendar-multiget on unloaded search results
tobixen Jun 14, 2026
82c4d37
refactor: unify search.py sync/async driver protocol
tobixen Jun 14, 2026
9613403
refactor: dedup rate-limit/get_calendars logic between sync/async
tobixen Jun 14, 2026
f831a21
refactor: dedup calendarobjectresource (a)sync twins
tobixen Jun 14, 2026
7b0ee75
refactor: dedup sync/async multistatus/multiget twins (§5.7)
tobixen Jun 14, 2026
adedcd7
refactor: collapse sync/async get_objects_by_sync_token twins
tobixen Jun 14, 2026
d735c73
refactor: accept generic <error>/<responsedescription>
tobixen Jun 14, 2026
72aab3e
docs: CHANGELOG for v3.3.0, and rewrite the http-libraries background
tobixen Jun 15, 2026
7c1aac9
fix: async Principal.calendar() did not work
tobixen Jun 16, 2026
0bb9c97
ci: bump pre-commit-hook versions to current latest
tobixen Jun 28, 2026
57cb0a0
docs: www.open-xchange.com 301 -> ox.io
tobixen Jul 26, 2026
3ddbc55
ci: let the link checker reuse and close a single report issue
tobixen Jul 26, 2026
6313122
ci: add pip-audit dependency audit as a tox env and a scheduled workflow
tobixen Aug 11, 2026
bc49105
chore: clear out two leftovers from the move off setuptools
tobixen Aug 11, 2026
775164b
ci: make the tox envlist take effect, and drop its bogus y39 entry
tobixen Aug 11, 2026
c09cf8b
test(nextcloud): maintenance of test server infrastructure
tobixen Aug 16, 2026
1b4535a
fix: A 207 Multi-Status containing only 404 is equivalent with 404
tobixen Aug 16, 2026
c1d9512
build: keep local junk out of the sdist, and check it in CI
tobixen Aug 16, 2026
4195436
test: make `enabled: false` actually disable a docker test server
tobixen Aug 16, 2026
7ba8542
docs: CHANGELOG accuracy fixes for v3.3.0
tobixen Aug 16, 2026
bf5048d
docs(tests): write down why testCheckCompatibility is not in CI
tobixen Aug 17, 2026
2331bd1
feat: support httpx2 as an async HTTP library
tobixen Aug 18, 2026
46622d8
test: enable ruff F841 and assert on the results tests were dropping
tobixen Aug 19, 2026
b558ed2
chore(compatibility): declare OX's rate limit so 429s get waited out
tobixen Aug 19, 2026
ac4fdc8
fix: say which object had no iCalendar in it, instead of ValueError f…
tobixen Aug 19, 2026
d25740a
test(scheduling): keep polling when a scheduling object is not readab…
tobixen Aug 19, 2026
7c03bd3
ci(deptry): ignore h2 under DEP003 as well as DEP001
tobixen Aug 19, 2026
eabc7fa
docs: drop stale Schedule-Tag TODOs and notes
tobixen Aug 20, 2026
d227492
docs: QA of the "feature-complete roadmap"
tobixen Aug 20, 2026
0149a4b
docs: explaining the Fragile for calendar-color
tobixen Aug 21, 2026
3b27afb
docs: comments and warnings on bare exception blocks
tobixen Aug 21, 2026
bc28477
chore: add a caldav[niquests] install target
tobixen Aug 21, 2026
95baecd
docs: design document consolidating the retry/resilience tickets
tobixen Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: audit

# Dependency vulnerability audit (pip-audit, see the `audit` env in tox.ini).
#
# This is deliberately *not* wired into the `tests` workflow: a new advisory
# can be published against an unchanged dependency tree, so the audit is
# time-triggered rather than change-triggered. The pull_request trigger is
# narrowed to the files that can change the dependency tree.
on:
pull_request:
paths:
- pyproject.toml
- tox.ini
- .github/workflows/audit.yml
workflow_dispatch:
schedule:
# Mondays 04:17 UTC, well clear of the nightly link check (22:03).
- cron: "17 4 * * 1"

# Least privilege for GITHUB_TOKEN: this workflow only reads the repository.
# Flagged by CodeQL, see https://github.com/python-caldav/caldav/pull/694
permissions:
contents: read

concurrency:
group: audit-${{ github.ref }}
cancel-in-progress: false

jobs:
pip-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
# hatch-vcs derives the version from git tags; without them the
# project metadata pip-audit reads cannot be built.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- name: Audit dependencies for known vulnerabilities
run: tox -e audit
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
54 changes: 45 additions & 9 deletions .github/workflows/linkcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,65 @@ on:
schedule:
- cron: "03 22 * * *"

concurrency:
group: linkcheck-${{ github.ref }}
cancel-in-progress: false

jobs:
linkcheck:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/checkout@v5
- name: Restore lychee cache
uses: actions/cache@v4
with:
path: .lycheecache
key: cache-lychee-${{ github.run_id }}
restore-keys: cache-lychee-
- name: Check links with Lychee
id: lychee
uses: lycheeverse/lychee-action@v2
with:
fail: false
args: >-
--root-dir "$(pwd)"
--timeout 20
--max-retries 3
--timeout 30
--max-retries 6
--retry-wait-time 2
--cache
--max-cache-age 14d
.
- name: Create Issue From File
if: steps.lychee.outputs.exit_code != 0
uses: peter-evans/create-issue-from-file@v5
with:
title: Link Checker Report
content-filepath: ./lychee/out.md
labels: report, automated issue
# The exit_code comparisons are quoted on purpose. A missing output is the
# empty string, and GitHub coerces '' to 0 when comparing against a number -
# so an unquoted `== 0` would treat "lychee did not run" as "all links are
# healthy" and close every open report. Comparing two strings does no
# coercion.
- name: Create or update Link Checker issue
if: steps.lychee.outputs.exit_code != '' && steps.lychee.outputs.exit_code != '0' && github.ref == 'refs/heads/master'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Keep the oldest open report issue as canonical, fold duplicates in.
# Both labels are required: `gh issue list` ANDs them, so a
# human-written issue that merely carries "report" cannot become the
# canonical one and have its body overwritten by lychee output.
ISSUES=$(gh issue list --label "report" --label "automated issue" --state open --json number --jq '.[].number' | sort -n)
CANON=$(printf '%s\n' "$ISSUES" | head -1)
for n in $(printf '%s\n' "$ISSUES" | tail -n +2); do
gh issue close "$n" --comment "Duplicate of #${CANON} - auto-closed by the link checker."
done
if [ -n "$CANON" ]; then
gh issue edit "$CANON" --body-file ./lychee/out.md
else
gh issue create --title "Link Checker Report" --body-file ./lychee/out.md --label "report" --label "automated issue"
fi
- name: Close Link Checker issue if all links are healthy
if: steps.lychee.outputs.exit_code == '0' && github.ref == 'refs/heads/master'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for n in $(gh issue list --label "report" --label "automated issue" --state open --json number --jq '.[].number'); do
gh issue close "$n" --comment "All links are now healthy."
done
62 changes: 62 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: package

# Builds the release artifacts and verifies their contents.
#
# Nothing in CI used to build an sdist at all, so what actually went into a
# release was only ever discovered after it was published: caldav-3.2.1.tar.gz
# shipped .claude/settings.json and 1755 files under venv/. The check runs on
# every change to the packaging configuration, and nightly, so a stray file in
# a contributor's tree cannot ride along into a tarball unnoticed.
#
# See tests/tools/check_dist.py for what is verified.
on:
push:
branches:
- master
pull_request:
paths:
- pyproject.toml
- tox.ini
- MANIFEST.in
- .gitignore
- tests/tools/check_dist.py
- .github/workflows/package.yml
workflow_dispatch:
schedule:
# Sundays 05:23 UTC, clear of the Monday audit (04:17) and the nightly
# link check (22:03).
- cron: "23 5 * * 0"

concurrency:
group: package-${{ github.ref }}
cancel-in-progress: true

# Least privilege for GITHUB_TOKEN: this workflow only reads the repository.
# Flagged by CodeQL, see https://github.com/python-caldav/caldav/pull/694
permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
# hatch-vcs derives the version from git tags.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- name: Build sdist and wheel, and check what is in them
run: tox -e package
- uses: actions/upload-artifact@v4
with:
name: dist
path: .tox/package/tmp/dist/*
if-no-files-found: error
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
72 changes: 61 additions & 11 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('setup.py') }}|${{ hashFiles('tox.ini') }}
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- name: Configure Baikal with pre-seeded database
run: |
Expand Down Expand Up @@ -183,13 +183,28 @@
docker exec ${{ job.services.nextcloud.id }} php occ config:system:set ratelimit.whitelist.0 --value='172.17.0.0/16' || true
docker exec ${{ job.services.nextcloud.id }} php occ config:system:set ratelimit.whitelist.1 --value='127.0.0.1' || true

# Clear rate limit cache
docker exec ${{ job.services.nextcloud.id }} php -r "
\$db = new PDO('sqlite:/var/www/html/data/nextcloud.db');
\$db->exec('DELETE FROM oc_ratelimit_entries');
\$db->exec('DELETE FROM oc_bruteforce_attempts');
echo 'Cleared rate limit and bruteforce caches\n';
" || true
# Clear rate limit cache. The SQLite file is named after the `dbname`
# config value, which defaults to `owncloud` — look it up rather than
# guessing, and check it exists first: PDO creates a missing SQLite
# file, so a wrong path silently yields "no such table" for every
# DELETE below.
DB_NAME=$(docker exec ${{ job.services.nextcloud.id }} php occ config:system:get dbname 2>/dev/null | tr -d '\r\n')
DB_PATH="/var/www/html/data/${DB_NAME:-owncloud}.db"
if docker exec ${{ job.services.nextcloud.id }} test -f "$DB_PATH"; then
docker exec ${{ job.services.nextcloud.id }} php -r "
\$db = new PDO('sqlite:$DB_PATH');
foreach (['oc_ratelimit_entries', 'oc_bruteforce_attempts'] as \$table) {
try {
\$db->exec(\"DELETE FROM \$table\");
} catch (PDOException \$e) {
fwrite(STDERR, \"skipping \$table: \" . \$e->getMessage() . \"\n\");
}
}
echo \"Cleared rate limit and bruteforce caches\n\";
" || true
else
echo "No database found at $DB_PATH — skipping cache cleanup"
fi

echo "Nextcloud is configured!"
- name: Configure Cyrus
Expand Down Expand Up @@ -326,7 +341,7 @@
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('setup.py') }}|${{ hashFiles('tox.ini') }}
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- run: tox -e docs
style:
Expand All @@ -339,7 +354,7 @@
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('setup.py') }}|${{ hashFiles('tox.ini') }}
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
Expand All @@ -356,7 +371,7 @@
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('setup.py') }}|${{ hashFiles('tox.ini') }}
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- run: tox -e deptry
# The three async-* jobs below exist to test the async backend *selection* logic,
Expand Down Expand Up @@ -418,7 +433,42 @@
"
- name: Run async tests with httpxyz
run: pytest tests/test_async_davclient.py -v
async-httpx2:
# Uninstalls niquests and httpxyz and installs httpx2 - Pydantic's
# continuation of httpx, a separate package rather than a new httpx release.
# Unlike httpxyz it does not register itself in sys.modules as "httpx", so
# this job is what catches code that reaches for httpx by name.
# Runs unit tests plus the Xandikos async integration tests (embedded, no
# service container needed), so the backend is exercised over real HTTP.
name: async (httpx2 fallback)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies with httpx2, without niquests or httpxyz
run: |
pip install --editable .[test]
pip uninstall -y niquests httpxyz httpx
pip install httpx2
- name: Verify httpx2 is used
run: |
python -c "
from caldav.async_davclient import _HTTPX_FLAVOUR, _USE_HTTPX, _USE_HTTPXYZ, _USE_NIQUESTS
assert not _USE_NIQUESTS, 'niquests should not be available'
assert not _USE_HTTPXYZ, 'httpxyz should not be available'
assert _USE_HTTPX, '_USE_HTTPX should be set when httpx2 is used'
assert _HTTPX_FLAVOUR == 'httpx2', f'expected httpx2, got {_HTTPX_FLAVOUR}'
print('✓ Using httpx2 for async HTTP')
"
- name: Run async tests with httpx2
# Xandikos runs embedded, so no service container is needed; the
# selection is deliberately narrow rather than "everything not baikal",
# since the runner has docker and would otherwise auto-discover the
# docker test servers.
run: pytest tests/test_async_davclient.py tests/test_async_integration.py -v -k "xandikos or Xandikos"
async-httpx:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium test

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
# Uninstalls both niquests and httpxyz to force the plain-httpx fallback path.
# Runs unit tests + a real integration test against Baikal (the lightest server)
# to verify end-to-end async HTTP with this backend.
Expand Down
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,23 @@ accountific.db
tests/.noseids
*.bak
*~
#*#
# Emacs auto-save files. The backslashes are required: a bare "#" starts a
# comment, so the pattern was silently a no-op.
\#*\#
caldav.egg-info/
tests/conf_private.py
.tox
.eggs
.venv
venv
caldav/_version.py
tests/docker-test-servers/baikal/baikal-backup/
tests/docker-test-servers/*/baikal-backup/
# But keep the pre-configured Specific directory for Baikal
!tests/docker-test-servers/baikal/Specific/
# Local test server configuration (may contain credentials)
tests/caldav_test_servers.yaml
# Lychee link checker cache
.lycheecache
# Scratch files from AI sessions (review notes, draft commit messages)
docs/design/tmp-*
6 changes: 6 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
https?://your\.server\.example\.com/.*
https?://.*\.example\.com(:\d+)?(/.*)?$
https?://domain/.*
https?://evil.attacker.com/caldav/

# Localhost URLs for test servers (not accessible in CI)
http://localhost:\d+/.*
Expand All @@ -17,6 +18,7 @@ https://caldav\.gmx\.net/.*
https://caldav\.icloud\.com/.*
https://p\d+-caldav\.icloud\.com/.*
https://posteo\.de:\d+/.*
https://sync\.infomaniak\.com/.*
https://purelymail\.com/.*
https://webmail\.all-inkl\.com/.*
https://www\.google\.com/calendar/dav/.*
Expand All @@ -36,6 +38,10 @@ https://oauth2\.googleapis\.com/.*
# Personal/demo test server (may be down)
https?://davical\.bekkenstenveien53c\.oslo\.no/.*

# Sites that serve 403 to non-browser clients. The links are fine in a
# browser; lychee just isn't one.
https://stackoverflow\.com/.*

# Dead or broken links we can't fix
http://fsf\.org/.*
http://oxpedia\.org/.*
Expand Down
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
---
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.4
rev: v0.15.20
hooks:
- id: ruff
- id: ruff-check
args: [--fix]
- id: ruff-format

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-byte-order-marker
- id: fix-byte-order-marker
- id: trailing-whitespace
- id: end-of-file-fixer

- repo: https://github.com/pycalendar/ai-prompt-auto-commit
rev: v0.0.5
rev: v0.0.8
hooks:
- id: unstage-ai-prompts
- id: append-ai-prompts
Expand All @@ -26,13 +26,13 @@ repos:
stages: [manual]

- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.4.0
rev: v4.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]

- repo: https://github.com/lycheeverse/lychee
rev: lychee-v0.24.1
rev: lychee-v0.24.2
hooks:
- id: lychee
args: ["--no-progress", "--timeout", "10", "--exclude-path", ".lycheeignore", "--max-cache-age=30d", "--cache"]
Expand Down
Loading
Loading