Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package Radicale for openSUSE:Factory checked in at 2025-11-17 12:18:51 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/Radicale (Old) and /work/SRC/openSUSE:Factory/.Radicale.new.2061 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "Radicale" Mon Nov 17 12:18:51 2025 rev:26 rq:1318071 version:3.5.8 Changes: -------- --- /work/SRC/openSUSE:Factory/Radicale/Radicale.changes 2025-10-05 17:52:12.277982802 +0200 +++ /work/SRC/openSUSE:Factory/.Radicale.new.2061/Radicale.changes 2025-11-17 12:24:05.551576792 +0100 @@ -1,0 +2,12 @@ +Sun Nov 9 11:44:40 UTC 2025 - Ákos Szőts <[email protected]> + +- Enable Argon2 support for testing and as a recommendation +- Update to 3.5.8 + * Extend [auth]: re-factor & overhaul LDAP authentication, especially for Python's ldap module + * Fix: out-of-range timestamp on 32-bit systems + * Fix: format_ut problem on 32-bit systems + * Feature: extend logging with response size in bytes and flag served as plain or gzip + * Feature: [storage] strict_preconditions: new config option to enforce strict preconditions check on PUT in case item already exists [RFC6352#9.2] + * Doc: Telugu translation + +------------------------------------------------------------------- Old: ---- v3.5.7.tar.gz New: ---- v3.5.8.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ Radicale.spec ++++++ --- /var/tmp/diff_new_pack.iMeBHQ/_old 2025-11-17 12:24:06.483616096 +0100 +++ /var/tmp/diff_new_pack.iMeBHQ/_new 2025-11-17 12:24:06.487616265 +0100 @@ -25,8 +25,9 @@ %define py_min_ver 3.9 %define vo_min_ver 0.9.6 %define pk_min_ver 1.1.0 +%define pt_min_ver 7 Name: Radicale -Version: 3.5.7 +Version: 3.5.8 Release: 0 Summary: A CalDAV calendar and CardDav contact server License: GPL-3.0-or-later @@ -41,11 +42,12 @@ BuildRequires: firewall-macros BuildRequires: pkgconfig BuildRequires: python-rpm-macros +BuildRequires: python3-argon2-cffi BuildRequires: python3-bcrypt BuildRequires: python3-defusedxml BuildRequires: python3-passlib BuildRequires: python3-pika -BuildRequires: python3-pytest +BuildRequires: python3-pytest >= %{pt_min_ver} BuildRequires: python3-setuptools BuildRequires: python3-vobject >= %{vo_min_ver} BuildRequires: python3-waitress @@ -59,6 +61,7 @@ Requires: python3-requests Requires: python3-vobject >= %{vo_min_ver} Recommends: apache2-utils +Recommends: python3-argon2-cffi Recommends: python3-bcrypt Recommends: python3-ldap3 BuildArch: noarch ++++++ v3.5.7.tar.gz -> v3.5.8.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/.github/workflows/docker-nightly-cleanup.yml new/Radicale-3.5.8/.github/workflows/docker-nightly-cleanup.yml --- old/Radicale-3.5.7/.github/workflows/docker-nightly-cleanup.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.5.8/.github/workflows/docker-nightly-cleanup.yml 2025-11-06 06:29:21.000000000 +0100 @@ -0,0 +1,41 @@ +name: Cleanup old nightly docker images + +on: + schedule: + - cron: '10 0 * * *' + workflow_dispatch: + + +jobs: + delete-package-versions: + name: Cleanup old nightly docker images + runs-on: ubuntu-latest + steps: + - name: Get list of all docker image versions in registry + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api --paginate -X GET "/orgs/Kozea/packages/container/Radicale/versions" -F package_type=container -F per_page=200 > data.json + + - name: Delete each nightly image older than cutoff date + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cutoff_date=$(date --date="30 days ago" --iso-8601) + echo "Cutoff date is: $cutoff_date" + + # Loop through each nightly image version (tag) older than the cutoff date + jq --arg cutoff_date "$cutoff_date" -r '.[] | select((.metadata.container.tags | any(. | contains("nightly"))) and (.created_at < $cutoff_date)) | [.metadata.container.tags[], .id] | @tsv' data.json | while IFS=$'\t' read -r tag nightly_image_id ; do + echo "Tag - $tag" + + # Because of multi-platform, manifest for each tag would contain more than 1 image. Loop through all + all_digests=$(docker manifest inspect "ghcr.io/kozea/radicale:${tag}" | jq -r 'if .manifests then .manifests[]?.digest else empty end') + for digest in $all_digests; do + image_id=$(jq -r --arg digest "$digest" '.[] | select(.name == $digest) | .id' data.json) + echo "Deleting $image_id" + gh api -X DELETE "/orgs/Kozea/packages/container/Radicale/versions/$image_id" + done + # Now that all dependents are deleted, delete this tag + echo "Deleting $tag with ID: $nightly_image_id" + gh api -X DELETE "/orgs/Kozea/packages/container/Radicale/versions/$nightly_image_id" + done diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/.github/workflows/docker-publish.yml new/Radicale-3.5.8/.github/workflows/docker-publish.yml --- old/Radicale-3.5.7/.github/workflows/docker-publish.yml 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/.github/workflows/docker-publish.yml 2025-11-06 06:29:21.000000000 +0100 @@ -2,7 +2,7 @@ on: release: - types: [published] + types: [released] schedule: - cron: '0 0 * * *' workflow_dispatch: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/.github/workflows/pypi-publish.yml new/Radicale-3.5.8/.github/workflows/pypi-publish.yml --- old/Radicale-3.5.7/.github/workflows/pypi-publish.yml 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/.github/workflows/pypi-publish.yml 2025-11-06 06:29:21.000000000 +0100 @@ -1,7 +1,7 @@ name: PyPI publish on: release: - types: [published] + types: [released] jobs: publish: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/.github/workflows/test.yml new/Radicale-3.5.8/.github/workflows/test.yml --- old/Radicale-3.5.7/.github/workflows/test.yml 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/.github/workflows/test.yml 2025-11-06 06:29:21.000000000 +0100 @@ -26,6 +26,7 @@ run: tox -c pyproject.toml -e py coveralls-test: + if: github.event_name == 'push' strategy: matrix: os: [ubuntu-latest] @@ -54,7 +55,6 @@ needs: coveralls-test if: github.event_name == 'push' runs-on: ubuntu-latest - continue-on-error: true steps: - uses: actions/setup-python@v5 with: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/CHANGELOG.md new/Radicale-3.5.8/CHANGELOG.md --- old/Radicale-3.5.7/CHANGELOG.md 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/CHANGELOG.md 2025-11-06 06:29:21.000000000 +0100 @@ -1,5 +1,13 @@ # Changelog +## 3.5.8 +* Extend [auth]: re-factor & overhaul LDAP authentication, especially for Python's ldap module +* Fix: out-of-range timestamp on 32-bit systems +* Feature: extend logging with response size in bytes and flag served as plain or gzip +* Feature: [storage] strict_preconditions: new config option to enforce strict preconditions check on PUT in case item already exists [RFC6352#9.2] +* Fix: format_ut problem on 32-bit systems +* Doc: Telugu translation + ## 3.5.7 * Extend: [auth] dovecot: add support for version >= 2.4 * Fix: report/getetag with enabled expand diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/DOCUMENTATION.md new/Radicale-3.5.8/DOCUMENTATION.md --- old/Radicale-3.5.7/DOCUMENTATION.md 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/DOCUMENTATION.md 2025-11-06 06:29:21.000000000 +0100 @@ -1,5 +1,9 @@ # Documentation +## Translations of this page + +* [Telugu](https://github.com/Kozea/Radicale/blob/master/docs/DOCUMENTATION.te.md) + ## Getting started #### About Radicale @@ -1523,6 +1527,14 @@ Default: `True` +##### strict_preconditions + +_(>= 3.5.8)_ + +Strict preconditions check on PUT in case item already exists [RFC6352#9.2](https://www.rfc-editor.org/rfc/rfc6352#section-9.2) + +Default: `False` + ##### hook Command that is run after changes to storage. See the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/config new/Radicale-3.5.8/config --- old/Radicale-3.5.7/config 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/config 2025-11-06 06:29:21.000000000 +0100 @@ -181,6 +181,9 @@ # Strip domain name from username #strip_domain = False +# URL Decode the given username (when URL-encoded by the client - useful for iOS devices when using email address) +#urldecode_username = False + [rights] @@ -197,8 +200,6 @@ # Permit overwrite of a collection (global) #permit_overwrite_collection = True -# URL Decode the given username (when URL-encoded by the client - useful for iOS devices when using email address) -# urldecode_username = False [storage] @@ -241,6 +242,9 @@ # Skip broken item instead of triggering an exception #skip_broken_item = True +# Strict preconditions check on PUT +#strict_preconditions = False + # Command that is run after changes to storage, default is emtpy # Supported placeholders: # %(user)s: logged-in user diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/contrib/logwatch/radicale new/Radicale-3.5.8/contrib/logwatch/radicale --- old/Radicale-3.5.7/contrib/logwatch/radicale 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/contrib/logwatch/radicale 2025-11-06 06:29:21.000000000 +0100 @@ -1,6 +1,6 @@ # This file is related to Radicale - CalDAV and CardDAV server # for logwatch (script) -# Copyright © 2024-2024 Peter Bieringer <[email protected]> +# Copyright © 2024-2025 Peter Bieringer <[email protected]> # # Detail levels # >= 5: Logins @@ -9,6 +9,7 @@ $Detail = $ENV{'LOGWATCH_DETAIL_LEVEL'} || 0; my %ResponseTimes; +my %ResponseSizes; my %Responses; my %Requests; my %Logins; @@ -39,6 +40,28 @@ $ResponseTimes{$req}->{'sum'} += $time; } +sub ResponseSizesMinMaxSum($$$) { + my $req = $_[0]; + my $type = $_[1]; + my $size = $_[2]; + + $ResponseSizes{$type}->{$req}->{'cnt'}++; + + if (! defined $ResponseSizes{$type}->{$req}->{'min'}) { + $ResponseSizes{$type}->{$req}->{'min'} = $size; + } elsif ($ResponseSizes{$type}->{$req}->{'min'} > $size) { + $ResponseSizes{$type}->{$req}->{'min'} = $size; + } + + if (! defined $ResponseSizes{$type}->{$req}->{'max'}) { + $ResponseSizes{$type}->{$req}{'max'} = $size; + } elsif ($ResponseSizes{$type}->{$req}->{'max'} < $size) { + $ResponseSizes{$type}->{$req}{'max'} = $size; + } + + $ResponseSizes{$type}->{$req}->{'sum'} += $size; +} + sub Sum($) { my $phash = $_[0]; my $sum = 0; @@ -75,9 +98,17 @@ if ( $ThisLine =~ / \S+ response status for .* with depth '(\d)' in ([0-9.]+) seconds: (\d+)/o ) { $req .= ":D=" . $1 . ":R=" . $3; ResponseTimesMinMaxSum($req, $2) if ($Detail >= 10); - } elsif ( $ThisLine =~ / \S+ response status for .* in ([0-9.]+) seconds: (\d+)/ ) { + } elsif ( $ThisLine =~ / \S+ response status for .* in ([0-9.]+) seconds: (\d+)/o ) { $req .= ":R=" . $2; ResponseTimesMinMaxSum($req, $1) if ($Detail >= 10); + } elsif ( $ThisLine =~ / \S+ response status for .* with depth '(\d)' in ([0-9.]+) seconds (\S+) (\d+) bytes: (\d+)/o ) { + $req .= ":D=" . $1 . ":R=" . $5; + ResponseTimesMinMaxSum($req, $2) if ($Detail >= 10); + ResponseSizesMinMaxSum($req, $3, $4) if ($Detail >= 10); + } elsif ( $ThisLine =~ / \S+ response status for .* in ([0-9.]+) seconds (\S+) (\d+) bytes: (\d+)/o ) { + $req .= ":R=" . $4; + ResponseTimesMinMaxSum($req, $1) if ($Detail >= 10); + ResponseSizesMinMaxSum($req, $2, $3) if ($Detail >= 10); } $Responses{$req}++; } @@ -174,6 +205,22 @@ print "-" x60 . "\n"; } +if (keys %ResponseSizes) { + for my $type (sort keys %ResponseSizes) { + print "\n**Response sizes (counts, bytes: $type) (D=<depth> R=<result>)**\n"; + printf "%-18s | %7s | %9s | %9s | %9s |\n", "Response", "cnt", "min", "max", "avg"; + print "-" x66 . "\n"; + foreach my $req (sort keys %{$ResponseSizes{$type}}) { + printf "%-18s | %7d | %9d | %9d | %9d |\n", $req + , $ResponseSizes{$type}->{$req}->{'cnt'} + , $ResponseSizes{$type}->{$req}->{'min'} + , $ResponseSizes{$type}->{$req}->{'max'} + , $ResponseSizes{$type}->{$req}->{'sum'} / $ResponseSizes{$type}->{$req}->{'cnt'}; + } + print "-" x66 . "\n"; + } +} + if (keys %OtherEvents) { print "\n**Other Events**\n"; foreach $ThisOne (sort keys %OtherEvents) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/docs/DOCUMENTATION.te.md new/Radicale-3.5.8/docs/DOCUMENTATION.te.md --- old/Radicale-3.5.7/docs/DOCUMENTATION.te.md 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.5.8/docs/DOCUMENTATION.te.md 2025-11-06 06:29:21.000000000 +0100 @@ -0,0 +1,288 @@ +> Last updated: 2025-10-20 by [@gowtham1412-p](https://github.com/gowtham1412-p) + +> Based on commit: [4fdc78760914040d5f74ece8978013b8836a712e] of [DOCUMENTATION.md](https://github.com/Kozea/Radicale/blob/master/DOCUMENTATION.md) + +\# డాక్యుమెంటేషన్ + + + +\## ప్రారంభించడం + + + +\#### రాడికేల్ గురించి + + + +రాడికేల్ అనేది ఒక చిన్న కానీ శక్తివంతమైన CalDAV (క్యాలెండర్లు, చేయవలసిన జాబితాలు) మరియు CardDAV + +(పరిచయాలు) సర్వర్, ఇది: + + + +\* CalDAV, CardDAV మరియు HTTP ద్వారా క్యాలెండర్లు మరియు పరిచయ జాబితాలను పంచుకుంటుంది. + +\* ఈవెంట్లు, టోడోలు, జర్నల్ ఎంట్రీలు మరియు వ్యాపార కార్డులకు మద్దతు ఇస్తుంది. + +\* బాక్స్ వెలుపల పనిచేస్తుంది, సంక్లిష్టమైన సెటప్ లేదా కాన్ఫిగరేషన్ అవసరం లేదు. + +\* సౌకర్యవంతమైన ప్రామాణీకరణ ఎంపికలను అందిస్తుంది. + +\* అధికారం ద్వారా యాక్సెస్ను పరిమితం చేయవచ్చు. + +\* TLSతో కనెక్షన్లను సురక్షితం చేయవచ్చు. + +\* చాలా మందితో పనిచేస్తుంది + +\[CalDAV మరియు CardDAV క్లయింట్లు](#సపోర్టెడ్-క్లయింట్లు). + +\* ఫైల్ సిస్టమ్లోని అన్ని డేటాను సాధారణ ఫోల్డర్ నిర్మాణంలో నిల్వ చేస్తుంది. + +\* ప్లగిన్లతో పొడిగించవచ్చు. + +\* GPLv3-లైసెన్స్ పొందిన ఉచిత సాఫ్ట్వేర్. + + + +\#### ఇన్స్టాలేషన్ + + + +తనిఖీ చేయండి + + + +\* \[ట్యుటోరియల్స్](#ట్యుటోరియల్స్) + +\* \[డాక్యుమెంటేషన్](#డాక్యుమెంటేషన్-1) + +\* \[GitHubలో వికీ](https://github.com/Kozea/Radicale/wiki) + +\* \[GitHubలో చర్చలు](https://github.com/Kozea/Radicale/discussions) + +\* \[GitHubలో తెరిచి ఉన్న మరియు ఇప్పటికే మూసివేయబడిన సమస్యలు](https://github.com/Kozea/Radicale/issues?q=is%3Aissue) + + + +\#### కొత్తగా ఏముంది? + + + +\[GitHubలో చేంజ్లాగ్](https://github.com/Kozea/Radicale/blob/master/CHANGELOG.md) చదవండి. + + + +\## ట్యుటోరియల్స్ + + + +\### 5 నిమిషాల సులభమైన సెటప్ + + + +మీరు Radicaleని ప్రయత్నించాలనుకుంటున్నారా కానీ మీ క్యాలెండర్లో 5 నిమిషాలు మాత్రమే ఖాళీగా ఉందా? + + + +ఇప్పుడే వెళ్లి Radicaleతో కొంచెం ఆడుదాం! + + + +ఈ విభాగం నుండి సెట్టింగ్లతో కాన్ఫిగర్ చేయబడిన సర్వర్, localhost + +కి మాత్రమే బైండ్ అవుతుంది (అంటే ఇది నెట్వర్క్ ద్వారా చేరుకోలేరు), మరియు మీరు ఏదైనా వినియోగదారు పేరు మరియు పాస్వర్డ్తో లాగిన్ అవ్వవచ్చు. + + + +ప్రతిదీ పనిచేసినప్పుడు, మీరు స్థానిక \[client](#supported-clients) + +ని పొందవచ్చు మరియు క్యాలెండర్లు మరియు చిరునామా పుస్తకాలను సృష్టించడం ప్రారంభించవచ్చు. + + + +Radicale మీ అవసరాలకు సరిపోతుంటే, రిమోట్ క్లయింట్లు మరియు కావలసిన ప్రామాణీకరణ రకానికి మద్దతు ఇవ్వడానికి కొంత \[ప్రాథమిక కాన్ఫిగరేషన్](#basic-configuration) + +కి సమయం కావచ్చు. + + + +మీ ఆపరేటింగ్ సిస్టమ్ను బట్టి దిగువన ఉన్న అధ్యాయాలలో ఒకదాన్ని అనుసరించండి. + + + +\#### Linux / \\\*BSD + + + +సూచన: PyPI నుండి డౌన్లోడ్ చేయడానికి బదులుగా, మీ \[distribution](#linux-distribution-packages) అందించిన ప్యాకేజీల కోసం చూడండి. + + + +అవి మీ పంపిణీలలో ఇంటిగ్రేట్ చేయబడిన స్టార్టప్ స్క్రిప్ట్లను కూడా కలిగి ఉంటాయి, ఇవి Radicaleని డెమోనైజ్ చేయడానికి అనుమతిస్తాయి. + + + +ముందుగా, \*\*python\*\* 3.9 లేదా తరువాత మరియు \*\*pip\*\* ఇన్స్టాల్ చేయబడిందని నిర్ధారించుకోండి. చాలా డిస్ట్రిబ్యూషన్లలో ``python3-pip`` ప్యాకేజీని ఇన్స్టాల్ చేయడానికి సరిపోతుంది. + + + +\##### సాధారణ వినియోగదారుగా + + + +పరీక్ష కోసం మాత్రమే సిఫార్సు చేయబడింది - కన్సోల్ను తెరిచి ఇలా టైప్ చేయండి: + + + +```bash + +\# ప్రస్తుత వినియోగదారు కోసం మాత్రమే ఇన్స్టాల్ చేయడానికి కింది ఆదేశాన్ని అమలు చేయండి + +python3 -m pip install --user --upgrade https://github.com/Kozea/Radicale/archive/master.tar.gz + +``` + + + +\_install\_ పని చేయకపోతే మరియు బదులుగా `error: externally-managed-environment` ప్రదర్శించబడితే, + +ముందుగానే వర్చువల్ వాతావరణాన్ని సృష్టించండి మరియు సక్రియం చేయండి. + + + +```bash + +python3 -m venv ~/venv + +source ~/venv/bin/activate + +``` + + + +మరియు దీనితో ఇన్స్టాల్ చేయడానికి ప్రయత్నించండి + + + +```bash + +python3 -m pip install --upgrade https://github.com/Kozea/Radicale/archive/master.tar.gz + +``` + + + +సేవను మాన్యువల్గా ప్రారంభించండి, డేటా ప్రస్తుత వినియోగదారు కోసం మాత్రమే నిల్వ చేయబడుతుంది + + + +```bash + +\# ప్రారంభించు, డేటా ప్రస్తుత వినియోగదారు కోసం మాత్రమే నిల్వ చేయబడుతుంది + +python3 -m radicale --storage-filesystem-folder=~/.var/lib/radicale/collections --auth-type none + +``` + + + +\#### సిస్టమ్ వినియోగదారుగా (లేదా రూట్గా) + + + +ప్రత్యామ్నాయంగా, మీరు సిస్టమ్ వినియోగదారుగా లేదా రూట్గా ఇన్స్టాల్ చేసి అమలు చేయవచ్చు (సిఫార్సు చేయబడలేదు): + + + +```bash + +\# కింది ఆదేశాన్ని రూట్ (సిఫార్సు చేయబడలేదు) లేదా రూట్ కాని వ్యవస్థ వినియోగదారుగా అమలు చేయండి + +\# (డిపెండెన్సీలు లేనప్పుడు తరువాతి వాటికి --user అవసరం కావచ్చు సిస్టమ్-వైడ్ మరియు/లేదా వర్చువల్ ఎన్విరాన్మెంట్ అందుబాటులో ఉంది) + +python3 -m pip install --upgrade https://github.com/Kozea/Radicale/archive/master.tar.gz + +``` + + + +`/var/lib/radicale/collections` కింద సిస్టమ్ ఫోల్డర్లో నిల్వ చేయబడిన డేటాతో సేవను మాన్యువల్గా ప్రారంభించండి: + + + +```bash + +\# Start, డేటా సిస్టమ్ ఫోల్డర్లో నిల్వ చేయబడుతుంది (/var/lib/radicale/collections కు వ్రాయడానికి అనుమతులు అవసరం) + +python3 -m radicale --storage-filesystem-folder=/var/lib/radicale/collections --auth-type none + +``` + + + +\#### Windows + + + +మొదటి దశ పైథాన్ను ఇన్స్టాల్ చేయడం. + +\[python.org](https://python.org) కు వెళ్లి పైథాన్ 3 యొక్క తాజా వెర్షన్ను డౌన్లోడ్ చేసుకోండి. + +తర్వాత ఇన్స్టాలర్ను అమలు చేయండి. + +ఇన్స్టాలర్ యొక్క మొదటి విండోలో, "PATH కు పైథాన్ను జోడించు" బాక్స్ను తనిఖీ చేసి, + +"ఇప్పుడే ఇన్స్టాల్ చేయి"పై క్లిక్ చేయండి. రెండు నిమిషాలు వేచి ఉండండి, పూర్తయింది! + + + +కమాండ్ ప్రాంప్ట్ను ప్రారంభించి ఇలా టైప్ చేయండి: + + + +```powershell + +python -m pip install --upgrade https://github.com/Kozea/Radicale/archive/master.tar.gz + +python -m radicale --storage-filesystem-folder=~/radicale/collections --auth-type none + +``` + + + +\##### Common + + + +విజయవంతం!!! మీ బ్రౌజర్లో <http://localhost:5232> తెరవండి! + +ఉదాహరణ ఎంపిక `--auth-type none` ద్వారా ప్రామాణీకరణ అవసరం లేనందున మీరు ఏదైనా వినియోగదారు పేరు మరియు పాస్వర్డ్తో లాగిన్ అవ్వవచ్చు. + +ఇది \*\*సురక్షితం\*\*, మరిన్ని వివరాల కోసం \[కాన్ఫిగరేషన్/ప్రామాణీకరణ](#auth) చూడండి. + + + +భద్రతా కారణాల దృష్ట్యా డిఫాల్ట్ కాన్ఫిగరేషన్ సర్వర్ను `localhost` (IPv4: `127.0.0.1`, IPv6: `::1`) కు బంధిస్తుందని గమనించండి. + + + +మరిన్ని వివరాల కోసం \[చిరునామాలు](#చిరునామాలు) మరియు \[కాన్ఫిగరేషన్/సర్వర్](#సర్వర్) చూడండి. + + + +\### ప్రాథమిక కాన్ఫిగరేషన్ + + + +ఇన్స్టాలేషన్ సూచనలను + +\[సరళమైన 5-నిమిషాల సెటప్](#సింపుల్-5-నిమిషాల-సెటప్) ట్యుటోరియల్లో చూడవచ్చు. + + + +రాడికేల్ `/etc/radicale/config` మరియు + +`~/.config/radicale/config` నుండి కాన్ఫిగరేషన్ ఫైల్లను లోడ్ చేయడానికి ప్రయత్నిస్తుంది. + +Cu + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/pyproject.toml new/Radicale-3.5.8/pyproject.toml --- old/Radicale-3.5.7/pyproject.toml 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/pyproject.toml 2025-11-06 06:29:21.000000000 +0100 @@ -3,7 +3,7 @@ # When the version is updated, a new section in the CHANGELOG.md file must be # added too. readme = "README.md" -version = "3.5.7" +version = "3.5.8" authors = [{name = "Guillaume Ayoub", email = "[email protected]"}, {name = "Unrud", email = "[email protected]"}, {name = "Peter Bieringer", email = "[email protected]"}] license = {text = "GNU GPL v3"} description = "CalDAV and CardDAV Server" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/app/__init__.py new/Radicale-3.5.8/radicale/app/__init__.py --- old/Radicale-3.5.7/radicale/app/__init__.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/app/__init__.py 2025-11-06 06:29:21.000000000 +0100 @@ -73,8 +73,6 @@ _web_type: str _script_name: str _extra_headers: Mapping[str, str] - _permit_delete_collection: bool - _permit_overwrite_collection: bool def __init__(self, configuration: config.Configuration) -> None: """Initialize Application. @@ -116,6 +114,8 @@ self._extra_headers = dict() for key in self.configuration.options("headers"): self._extra_headers[key] = configuration.get("headers", key) + self._strict_preconditions = configuration.get("storage", "strict_preconditions") + logger.info("strict preconditions check: %s", self._strict_preconditions) def _scrub_headers(self, environ: types.WSGIEnviron) -> types.WSGIEnviron: """Mask passwords and cookies.""" @@ -164,6 +164,7 @@ answer: Union[None, str, bytes]) -> _IntermediateResponse: """Helper to create response from internal types.WSGIResponse""" headers = dict(headers) + content_encoding = "plain" # Set content length answers = [] if answer is not None: @@ -183,6 +184,7 @@ zcomp = zlib.compressobj(wbits=16 + zlib.MAX_WBITS) answer = zcomp.compress(answer) + zcomp.flush() headers["Content-Encoding"] = "gzip" + content_encoding = "gzip" headers["Content-Length"] = str(len(answer)) answers.append(answer) @@ -194,9 +196,14 @@ time_end = datetime.datetime.now() status_text = "%d %s" % ( status, client.responses.get(status, "Unknown")) - logger.info("%s response status for %r%s in %.3f seconds: %s", - request_method, unsafe_path, depthinfo, - (time_end - time_begin).total_seconds(), status_text) + if answer is not None: + logger.info("%s response status for %r%s in %.3f seconds %s %s bytes: %s", + request_method, unsafe_path, depthinfo, + (time_end - time_begin).total_seconds(), content_encoding, str(len(answer)), status_text) + else: + logger.info("%s response status for %r%s in %.3f seconds: %s", + request_method, unsafe_path, depthinfo, + (time_end - time_begin).total_seconds(), status_text) # Return response content return status_text, list(headers.items()), answers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/app/base.py new/Radicale-3.5.8/radicale/app/base.py --- old/Radicale-3.5.7/radicale/app/base.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/app/base.py 2025-11-06 06:29:21.000000000 +0100 @@ -41,6 +41,7 @@ _encoding: str _permit_delete_collection: bool _permit_overwrite_collection: bool + _strict_preconditions: bool _hook: hook.BaseHook def __init__(self, configuration: config.Configuration) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/app/put.py new/Radicale-3.5.8/radicale/app/put.py --- old/Radicale-3.5.7/radicale/app/put.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/app/put.py 2025-11-06 06:29:21.000000000 +0100 @@ -207,6 +207,9 @@ return httputils.NOT_ALLOWED etag = environ.get("HTTP_IF_MATCH", "") + if item and not etag and self._strict_preconditions: + logger.warning("Precondition failed for %r: existing item, no If-Match header, strict mode enabled", path) + return httputils.PRECONDITION_FAILED if not item and etag: # Etag asked but no item found: item has been removed logger.warning("Precondition failed on PUT request for %r (HTTP_IF_MATCH: %s, item not existing)", path, etag) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/auth/ldap.py new/Radicale-3.5.8/radicale/auth/ldap.py --- old/Radicale-3.5.7/radicale/auth/ldap.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/auth/ldap.py 2025-11-06 06:29:21.000000000 +0100 @@ -1,6 +1,7 @@ # This file is part of Radicale - CalDAV and CardDAV server # Copyright © 2022-2024 Peter Varkoly # Copyright © 2024-2024 Peter Bieringer <[email protected]> +# Copyright © 2024-2025 Peter Marschall <[email protected]> # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -67,10 +68,8 @@ _ldap_group_filter: str _ldap_group_members_attr: str _ldap_module_version: int = 3 - _use_encryption: bool = False - _ldap_use_ssl: bool = False _ldap_security: str = "none" - _ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED + _ldap_ssl_verify_mode: str = "REQUIRED" _ldap_ssl_ca_file: str = "" def __init__(self, configuration: config.Configuration) -> None: @@ -85,7 +84,7 @@ self._ldap_module_version = 2 self.ldap = ldap except ImportError as e: - raise RuntimeError("LDAP authentication requires the ldap3 module") from e + raise RuntimeError("LDAP authentication requires the ldap3 or ldap module") from e self._ldap_ignore_attribute_create_modify_timestamp = configuration.get("auth", "ldap_ignore_attribute_create_modify_timestamp") self._ldap_uri = configuration.get("auth", "ldap_uri") @@ -102,66 +101,77 @@ if ldap_secret_file_path: with open(ldap_secret_file_path, 'r') as file: self._ldap_secret = file.read().rstrip('\n') - if self._ldap_module_version == 3: - self._ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") - self._ldap_security = configuration.get("auth", "ldap_security") - self._use_encryption = self._ldap_use_ssl or self._ldap_security in ("tls", "starttls") - if self._ldap_use_ssl and self._ldap_security == "starttls": - raise RuntimeError("Cannot set both 'ldap_use_ssl = True' and 'ldap_security' = 'starttls'") - if self._ldap_use_ssl: - logger.warning("Configuration uses soon to be deprecated 'ldap_use_ssl', use 'ldap_security' ('none', 'tls', 'starttls') instead.") - if self._use_encryption: - self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file") - tmp = configuration.get("auth", "ldap_ssl_verify_mode") - if tmp == "NONE": - self._ldap_ssl_verify_mode = ssl.CERT_NONE - elif tmp == "OPTIONAL": - self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL - - logger.info("auth.ldap_uri : %r" % self._ldap_uri) - logger.info("auth.ldap_base : %r" % self._ldap_base) - logger.info("auth.ldap_reader_dn : %r" % self._ldap_reader_dn) - logger.info("auth.ldap_filter : %r" % self._ldap_filter) + self._ldap_security = configuration.get("auth", "ldap_security") + if self._ldap_security not in ("none", "tls", "starttls"): + raise RuntimeError("Illegal value for config setting ´ldap_security'") + ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") + if ldap_use_ssl: + logger.warning("Configuration uses deprecated 'ldap_use_ssl': use 'ldap_security' ('none', 'tls', 'starttls') instead.") + if self._ldap_security == "starttls": + raise RuntimeError("Deprecated config setting 'ldap_use_ssl = True' conflicts with 'ldap_security' = 'starttls'") + elif self._ldap_security != "tls": + logger.warning("Update configuration: set 'ldap_security = tls' instead of deprecated 'ldap_use_ssl = True'") + self._ldap_security = "tls" + self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file") + self._ldap_ssl_verify_mode = configuration.get("auth", "ldap_ssl_verify_mode") + if self._ldap_ssl_verify_mode not in ("NONE", "OPTIONAL", "REQUIRED"): + raise RuntimeError("Illegal value for config setting ´ldap_ssl_verify_mode'") + + if self._ldap_uri.lower().startswith("ldaps://") and self._ldap_security not in ("tls", "starttls"): + logger.info("Inferring 'ldap_security' = tls from 'ldap_uri' starting with 'ldaps://'") + self._ldap_security = "tls" + if self._ldap_uri.lower().startswith("ldapi://") and self._ldap_ssl_verify_mode != "NONE": + logger.info("Lowering 'ldap_'ldap_ssl_verify_mode' to NONE for 'ldap_uri' starting with 'ldapi://'") + self._ldap_ssl_verify_mode = "NONE" + + if self._ldap_ssl_ca_file == "" and self._ldap_ssl_verify_mode != "NONE" and self._ldap_security in ("tls", "starttls"): + logger.warning("Certificate verification not possible: 'ldap_ssl_ca_file' not set") + if self._ldap_ssl_ca_file and self._ldap_security not in ("tls", "starttls"): + logger.warning("Config setting 'ldap_ssl_ca_file' useless without encrypted LDAP connection") + + logger.info("auth.ldap_uri : %r" % self._ldap_uri) + logger.info("auth.ldap_base : %r" % self._ldap_base) + logger.info("auth.ldap_reader_dn : %r" % self._ldap_reader_dn) + logger.info("auth.ldap_filter : %r" % self._ldap_filter) if self._ldap_user_attr: - logger.info("auth.ldap_user_attribute : %r" % self._ldap_user_attr) + logger.info("auth.ldap_user_attribute : %r" % self._ldap_user_attr) else: - logger.info("auth.ldap_user_attribute : (not provided)") + logger.info("auth.ldap_user_attribute : (not provided)") if self._ldap_groups_attr: - logger.info("auth.ldap_groups_attribute: %r" % self._ldap_groups_attr) + logger.info("auth.ldap_groups_attribute : %r" % self._ldap_groups_attr) else: - logger.info("auth.ldap_groups_attribute: (not provided)") + logger.info("auth.ldap_groups_attribute : (not provided)") if self._ldap_group_base: - logger.info("auth.ldap_group_base : %r" % self._ldap_group_base) + logger.info("auth.ldap_group_base : %r" % self._ldap_group_base) else: - logger.info("auth.ldap_group_base : (not provided, using ldap_base)") + logger.info("auth.ldap_group_base : (not provided, using ldap_base)") self._ldap_group_base = self._ldap_base if self._ldap_group_filter: - logger.info("auth.ldap_group_filter: %r" % self._ldap_group_filter) + logger.info("auth.ldap_group_filter : %r" % self._ldap_group_filter) else: - logger.info("auth.ldap_group_filter: (not provided)") + logger.info("auth.ldap_group_filter : (not provided)") if self._ldap_group_members_attr: logger.info("auth.ldap_group_members_attr: %r" % self._ldap_group_members_attr) else: logger.info("auth.ldap_group_members_attr: (not provided)") if ldap_secret_file_path: - logger.info("auth.ldap_secret_file_path: %r" % ldap_secret_file_path) + logger.info("auth.ldap_secret_file_path : %r" % ldap_secret_file_path) if self._ldap_secret: - logger.info("auth.ldap_secret : (from file)") + logger.info("auth.ldap_secret : (from file)") else: - logger.info("auth.ldap_secret_file_path: (not provided)") + logger.info("auth.ldap_secret_file_path : (not provided)") if self._ldap_secret: - logger.info("auth.ldap_secret : (from config)") + logger.info("auth.ldap_secret : (from config)") if self._ldap_reader_dn and not self._ldap_secret: - logger.error("auth.ldap_secret : (not provided)") + logger.error("auth.ldap_secret : (not provided)") raise RuntimeError("LDAP authentication requires ldap_secret for ldap_reader_dn") - logger.info("auth.ldap_use_ssl : %s" % self._ldap_use_ssl) - logger.info("auth.ldap_security : %s" % self._ldap_security) - if self._use_encryption: - logger.info("auth.ldap_ssl_verify_mode : %s" % self._ldap_ssl_verify_mode) - if self._ldap_ssl_ca_file: - logger.info("auth.ldap_ssl_ca_file : %r" % self._ldap_ssl_ca_file) - else: - logger.info("auth.ldap_ssl_ca_file : (not provided)") + logger.info("auth.ldap_use_ssl : %s" % ldap_use_ssl) + logger.info("auth.ldap_security : %s" % self._ldap_security) + logger.info("auth.ldap_ssl_verify_mode : %s" % self._ldap_ssl_verify_mode) + if self._ldap_ssl_ca_file: + logger.info("auth.ldap_ssl_ca_file : %r" % self._ldap_ssl_ca_file) + else: + logger.info("auth.ldap_ssl_ca_file : (not provided)") if self._ldap_ignore_attribute_create_modify_timestamp: logger.info("auth.ldap_ignore_attribute_create_modify_timestamp applied (relevant for ldap3 only)") """Extend attributes to to be returned in the user query""" @@ -169,15 +179,31 @@ self._ldap_attributes.append(self._ldap_groups_attr) if self._ldap_user_attr: self._ldap_attributes.append(self._ldap_user_attr) - logger.info("ldap_attributes : %r" % self._ldap_attributes) + logger.info("ldap_attributes : %r" % self._ldap_attributes) def _login2(self, login: str, password: str) -> str: try: """Bind as reader dn""" logger.debug(f"_login2 {self._ldap_uri}, {self._ldap_reader_dn}") conn = self.ldap.initialize(self._ldap_uri) - conn.protocol_version = 3 + conn.protocol_version = self.ldap.VERSION3 conn.set_option(self.ldap.OPT_REFERRALS, 0) + + if self._ldap_security in ("tls", "starttls"): + """certificate validation mode""" + verifyMode = {"NONE": self.ldap.OPT_X_TLS_NEVER, + "OPTIONAL": self.ldap.OPT_X_TLS_ALLOW, + "REQUIRED": self.ldap.OPT_X_TLS_DEMAND} + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, verifyMode[self._ldap_ssl_verify_mode]) + """CA file to validate certificate against""" + if self._ldap_ssl_ca_file: + conn.set_option(self.ldap.OPT_X_TLS_CACERTFILE, self._ldap_ssl_ca_file) + """create TLS context- this must be the last TLS setting""" + conn.set_option(self.ldap.OPT_X_TLS_NEWCTX, self.ldap.OPT_ON) + + if self._ldap_security == "starttls": + conn.start_tls_s() + conn.simple_bind_s(self._ldap_reader_dn, self._ldap_secret) """Search for the dn of user to authenticate""" escaped_login = self.ldap.filter.escape_filter_chars(login) @@ -219,16 +245,11 @@ for dn, entry in res: groupDNs.append(dn) - """Close LDAP connection""" - conn.unbind() except Exception as e: raise RuntimeError(f"Invalid LDAP configuration:{e}") try: """Bind as user to authenticate""" - conn = self.ldap.initialize(self._ldap_uri) - conn.protocol_version = 3 - conn.set_option(self.ldap.OPT_REFERRALS, 0) conn.simple_bind_s(user_dn, password) if self._ldap_user_attr: if user_entry[1][self._ldap_user_attr]: @@ -263,15 +284,15 @@ """Connect the server""" try: logger.debug(f"_login3 {self._ldap_uri}, {self._ldap_reader_dn}") - if self._use_encryption: + if self._ldap_security in ("tls", "starttls"): logger.debug("_login3 using encryption (reader)") - tls = self.ldap3.Tls(validate=self._ldap_ssl_verify_mode) + verifyMode = {"NONE": ssl.CERT_NONE, + "OPTIONAL": ssl.CERT_OPTIONAL, + "REQUIRED": ssl.CERT_REQUIRED} + tls = self.ldap3.Tls(validate=verifyMode[self._ldap_ssl_verify_mode]) if self._ldap_ssl_ca_file != "": - tls = self.ldap3.Tls( - validate=self._ldap_ssl_verify_mode, - ca_certs_file=self._ldap_ssl_ca_file - ) - if self._ldap_use_ssl or self._ldap_security == "tls": + tls = self.ldap3.Tls(validate=verifyMode[self._ldap_ssl_verify_mode], ca_certs_file=self._ldap_ssl_ca_file) + if self._ldap_security == "tls": logger.debug("_login3 using ssl (reader)") server = self.ldap3.Server(self._ldap_uri, use_ssl=True, tls=tls) else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/config.py new/Radicale-3.5.8/radicale/config.py --- old/Radicale-3.5.7/radicale/config.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/config.py 2025-11-06 06:29:21.000000000 +0100 @@ -430,6 +430,10 @@ "value": "", "help": "command that is run after changes to storage", "type": str}), + ("strict_preconditions", { + "value": "False", + "help": "strict preconditions check on PUT", + "type": bool}), ("_filesystem_fsync", { "value": "True", "help": "sync all changes to filesystem during requests", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/tests/__init__.py new/Radicale-3.5.8/radicale/tests/__init__.py --- old/Radicale-3.5.7/radicale/tests/__init__.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/tests/__init__.py 2025-11-06 06:29:21.000000000 +0100 @@ -75,6 +75,10 @@ if login is not None and not isinstance(login, str): raise TypeError("login argument must be %r, not %r" % (str, type(login))) + http_if_match = kwargs.pop("http_if_match", None) + if http_if_match is not None and not isinstance(http_if_match, str): + raise TypeError("http_if_match argument must be %r, not %r" % + (str, type(http_if_match))) environ: Dict[str, Any] = {k.upper(): v for k, v in kwargs.items()} for k, v in environ.items(): if not isinstance(v, str): @@ -84,6 +88,8 @@ if login: environ["HTTP_AUTHORIZATION"] = "Basic " + base64.b64encode( login.encode(encoding)).decode() + if http_if_match: + environ["HTTP_IF_MATCH"] = http_if_match environ["REQUEST_METHOD"] = method.upper() environ["PATH_INFO"] = path if data is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/tests/test_base.py new/Radicale-3.5.8/radicale/tests/test_base.py --- old/Radicale-3.5.7/radicale/tests/test_base.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/tests/test_base.py 2025-11-06 06:29:21.000000000 +0100 @@ -229,6 +229,56 @@ _, answer = self.get(path) assert "DTSTAMP:20130902T150159Z" in answer + def test_update_event_no_etag_strict_preconditions_true(self) -> None: + """Update an event without serving etag having strict_preconditions enabled (Precondition Failed).""" + self.configure({"storage": {"strict_preconditions": True}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1.ics") + event_modified = get_file_content("event1_modified.ics") + path = "/calendar.ics/event1.ics" + self.put(path, event, check=201) + self.put(path, event_modified, check=412) + + def test_update_event_with_etag_strict_preconditions_true(self) -> None: + """Update an event with serving equal etag having strict_preconditions enabled (OK).""" + self.configure({"storage": {"strict_preconditions": True}}) + self.configure({"logging": {"response_content_on_debug": True}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1.ics") + event_modified = get_file_content("event1_modified.ics") + path = "/calendar.ics/event1.ics" + self.put(path, event, check=201) + # get etag + _, responses = self.report("/calendar.ics/", """\ +<?xml version="1.0" encoding="utf-8" ?> +<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav"> + <D:prop xmlns:D="DAV:"> + <D:getetag/> + </D:prop> +</C:calendar-query>""") + assert len(responses) == 1 + response = responses["/calendar.ics/event1.ics"] + assert not isinstance(response, int) + status, prop = response["D:getetag"] + assert status == 200 and prop.text + self.put(path, event_modified, check=204, http_if_match=prop.text) + + def test_update_event_with_etag_mismatch(self) -> None: + """Update an event with serving mismatch etag (Precondition Failed).""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1.ics") + event_modified = get_file_content("event1_modified.ics") + path = "/calendar.ics/event1.ics" + self.put(path, event, check=201) + self.put(path, event_modified, check=412, http_if_match="0000") + + def test_add_event_with_etag(self) -> None: + """Add an event with serving etag (Precondition Failed).""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1.ics") + path = "/calendar.ics/event1.ics" + self.put(path, event, check=412, http_if_match="0000") + def test_update_event_uid_event(self) -> None: """Update an event with a different UID.""" self.mkcalendar("/calendar.ics/") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/radicale/utils.py new/Radicale-3.5.8/radicale/utils.py --- old/Radicale-3.5.7/radicale/utils.py 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/radicale/utils.py 2025-11-06 06:29:21.000000000 +0100 @@ -47,8 +47,9 @@ Tuple[str, int, int, int]] -# Max YEAR in datetime in unixtime +# Max/Min YEAR in datetime in unixtime DATETIME_MAX_UNIXTIME: int = (datetime.MAXYEAR - 1970) * 365 * 24 * 60 * 60 +DATETIME_MIN_UNIXTIME: int = (datetime.MINYEAR - 1970) * 365 * 24 * 60 * 60 def load_plugin(internal_types: Sequence[str], module_name: str, @@ -279,12 +280,14 @@ if sys.platform == "win32": # TODO check how to support this better return str(unixtime) - if unixtime < DATETIME_MAX_UNIXTIME: + if unixtime <= DATETIME_MIN_UNIXTIME: + r = str(unixtime) + "(<=MIN:" + str(DATETIME_MIN_UNIXTIME) + ")" + elif unixtime >= DATETIME_MAX_UNIXTIME: + r = str(unixtime) + "(>=MAX:" + str(DATETIME_MAX_UNIXTIME) + ")" + else: if sys.version_info < (3, 11): dt = datetime.datetime.utcfromtimestamp(unixtime) else: dt = datetime.datetime.fromtimestamp(unixtime, datetime.UTC) r = str(unixtime) + "(" + dt.strftime('%Y-%m-%dT%H:%M:%SZ') + ")" - else: - r = str(unixtime) + "(>MAX:" + str(DATETIME_MAX_UNIXTIME) + ")" return r diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.5.7/setup.py.legacy new/Radicale-3.5.8/setup.py.legacy --- old/Radicale-3.5.7/setup.py.legacy 2025-09-27 08:16:00.000000000 +0200 +++ new/Radicale-3.5.8/setup.py.legacy 2025-11-06 06:29:21.000000000 +0100 @@ -20,7 +20,7 @@ # When the version is updated, a new section in the CHANGELOG.md file must be # added too. -VERSION = "3.5.7" +VERSION = "3.5.8" with open("README.md", encoding="utf-8") as f: long_description = f.read()
