1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-06-26 16:45:52 +00:00

Merge branch 'master' into react0r

This commit is contained in:
ray-react0r 2024-08-15 15:07:49 -06:00 committed by GitHub
commit 3cba4b32a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 134 additions and 59 deletions

View file

@ -6,7 +6,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', pypy-3.8, pypy-3.9] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12.3', '3.13.0-beta.4', pypy-3.8, pypy-3.9]
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: pypy-3.8 python-version: pypy-3.8
@ -21,7 +21,7 @@ jobs:
- name: Install Test dependencies - name: Install Test dependencies
run: pip install tox run: pip install tox
- name: Test - name: Test
run: tox run: tox -e py
- name: Install Coveralls - name: Install Coveralls
if: github.event_name == 'push' if: github.event_name == 'push'
run: pip install coveralls run: pip install coveralls
@ -46,3 +46,15 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: coveralls --service=github --finish run: coveralls --service=github --finish
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install tox
run: pip install tox
- name: Lint
run: tox -e flake8,mypy,isort

View file

@ -4,6 +4,8 @@
* Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar * Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar
* Enhancement: Added free-busy report * Enhancement: Added free-busy report
* Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS on reports * Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS on reports
* Enhancement: remove unexpected control codes from uploaded items
* Drop: remove unused requirement "typeguard"
* Improve: Refactored some date parsing code * Improve: Refactored some date parsing code
## 3.2.2 ## 3.2.2

View file

@ -350,16 +350,13 @@ location /radicale/ { # The trailing / is important!
} }
``` ```
Example **Caddy** configuration with basicauth from Caddy: Example **Caddy** configuration:
```Caddy ```
handle_path /radicale* { handle_path /radicale/* {
basicauth { uri strip_prefix /radicale
user hash
}
reverse_proxy localhost:5232 { reverse_proxy localhost:5232 {
header_up +X-Script-Name "/radicale" header_up X-Script-Name /radicale
header_up +X-remote-user "{http.auth.user.id}"
} }
} }
``` ```
@ -440,6 +437,21 @@ location /radicale/ {
} }
``` ```
Example **Caddy** configuration:
```
handle_path /radicale/* {
uri strip_prefix /radicale
basicauth {
USER HASH
}
reverse_proxy localhost:5232 {
header_up X-Script-Name /radicale
header_up X-remote-user {http.auth.user.id}
}
}
```
Example **Apache** configuration: Example **Apache** configuration:
```apache ```apache
@ -795,6 +807,12 @@ providers like ldap, kerberos
Default: `False` Default: `False`
##### strip_domain
Strip domain from username
Default: `False`
#### rights #### rights
##### type ##### type
@ -865,7 +883,7 @@ Delete sync-token that are older than the specified time. (seconds)
Default: `2592000` Default: `2592000`
#### skip_broken_item ##### skip_broken_item
Skip broken item instead of triggering an exception Skip broken item instead of triggering an exception

2
config
View file

@ -73,6 +73,8 @@
# Convert username to lowercase, must be true for case-insensitive auth providers # Convert username to lowercase, must be true for case-insensitive auth providers
#lc_username = False #lc_username = False
# Strip domain name from username
#strip_domain = False
[rights] [rights]

View file

@ -61,7 +61,7 @@ def _get_application_instance(config_path: str, wsgi_errors: types.ErrorStream
if not miss and source != "default config": if not miss and source != "default config":
default_config_active = False default_config_active = False
if default_config_active: if default_config_active:
logger.warn("%s", "No config file found/readable - only default config is active") logger.warning("%s", "No config file found/readable - only default config is active")
_application_instance = Application(configuration) _application_instance = Application(configuration)
if _application_config_path != config_path: if _application_config_path != config_path:
raise ValueError("RADICALE_CONFIG must not change: %r != %r" % raise ValueError("RADICALE_CONFIG must not change: %r != %r" %

View file

@ -175,7 +175,7 @@ def run() -> None:
default_config_active = False default_config_active = False
if default_config_active: if default_config_active:
logger.warn("%s", "No config file found/readable - only default config is active") logger.warning("%s", "No config file found/readable - only default config is active")
if args_ns.verify_storage: if args_ns.verify_storage:
logger.info("Verifying storage") logger.info("Verifying storage")
@ -183,7 +183,7 @@ def run() -> None:
storage_ = storage.load(configuration) storage_ = storage.load(configuration)
with storage_.acquire_lock("r"): with storage_.acquire_lock("r"):
if not storage_.verify(): if not storage_.verify():
logger.critical("Storage verifcation failed") logger.critical("Storage verification failed")
sys.exit(1) sys.exit(1)
except Exception as e: except Exception as e:
logger.critical("An exception occurred during storage " logger.critical("An exception occurred during storage "

View file

@ -232,7 +232,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
path.rstrip("/").endswith("/.well-known/carddav")): path.rstrip("/").endswith("/.well-known/carddav")):
return response(*httputils.redirect( return response(*httputils.redirect(
base_prefix + "/", client.MOVED_PERMANENTLY)) base_prefix + "/", client.MOVED_PERMANENTLY))
# Return NOT FOUND for all other paths containing ".well-knwon" # Return NOT FOUND for all other paths containing ".well-known"
if path.endswith("/.well-known") or "/.well-known/" in path: if path.endswith("/.well-known") or "/.well-known/" in path:
return response(*httputils.NOT_FOUND) return response(*httputils.NOT_FOUND)

View file

@ -322,13 +322,13 @@ def xml_propfind_response(
responses[404 if is404 else 200].append(element) responses[404 if is404 else 200].append(element)
for status_code, childs in responses.items(): for status_code, children in responses.items():
if not childs: if not children:
continue continue
propstat = ET.Element(xmlutils.make_clark("D:propstat")) propstat = ET.Element(xmlutils.make_clark("D:propstat"))
response.append(propstat) response.append(propstat)
prop = ET.Element(xmlutils.make_clark("D:prop")) prop = ET.Element(xmlutils.make_clark("D:prop"))
prop.extend(childs) prop.extend(children)
propstat.append(prop) propstat.append(prop)
status = ET.Element(xmlutils.make_clark("D:status")) status = ET.Element(xmlutils.make_clark("D:status"))
status.text = xmlutils.make_response(status_code) status.text = xmlutils.make_response(status_code)

View file

@ -363,7 +363,7 @@ def _make_vobject_expanded_item(
if hasattr(item.vobject_item.vevent, 'rrule'): if hasattr(item.vobject_item.vevent, 'rrule'):
rruleset = vevent.getrruleset() rruleset = vevent.getrruleset()
# There is something strage behavour during serialization native datetime, so converting manualy # There is something strange behaviour during serialization native datetime, so converting manually
vevent.dtstart.value = vevent.dtstart.value.strftime(dt_format) vevent.dtstart.value = vevent.dtstart.value.strftime(dt_format)
if dt_end is not None: if dt_end is not None:
vevent.dtend.value = vevent.dtend.value.strftime(dt_format) vevent.dtend.value = vevent.dtend.value.strftime(dt_format)

View file

@ -52,6 +52,7 @@ def load(configuration: "config.Configuration") -> "BaseAuth":
class BaseAuth: class BaseAuth:
_lc_username: bool _lc_username: bool
_strip_domain: bool
def __init__(self, configuration: "config.Configuration") -> None: def __init__(self, configuration: "config.Configuration") -> None:
"""Initialize BaseAuth. """Initialize BaseAuth.
@ -63,6 +64,7 @@ class BaseAuth:
""" """
self.configuration = configuration self.configuration = configuration
self._lc_username = configuration.get("auth", "lc_username") self._lc_username = configuration.get("auth", "lc_username")
self._strip_domain = configuration.get("auth", "strip_domain")
def get_external_login(self, environ: types.WSGIEnviron) -> Union[ def get_external_login(self, environ: types.WSGIEnviron) -> Union[
Tuple[()], Tuple[str, str]]: Tuple[()], Tuple[str, str]]:
@ -91,4 +93,8 @@ class BaseAuth:
raise NotImplementedError raise NotImplementedError
def login(self, login: str, password: str) -> str: def login(self, login: str, password: str) -> str:
return self._login(login, password).lower() if self._lc_username else self._login(login, password) if self._lc_username:
login = login.lower()
if self._strip_domain:
login = login.split('@')[0]
return self._login(login, password)

View file

@ -36,7 +36,7 @@ pointed to by the ``htpasswd_filename`` configuration value while assuming
the password encryption method specified via the ``htpasswd_encryption`` the password encryption method specified via the ``htpasswd_encryption``
configuration value. configuration value.
The following htpasswd password encrpytion methods are supported by Radicale The following htpasswd password encryption methods are supported by Radicale
out-of-the-box: out-of-the-box:
- plain-text (created by htpasswd -p ...) -- INSECURE - plain-text (created by htpasswd -p ...) -- INSECURE
- MD5-APR1 (htpasswd -m ...) -- htpasswd's default method, INSECURE - MD5-APR1 (htpasswd -m ...) -- htpasswd's default method, INSECURE

View file

@ -191,6 +191,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"value": "1", "value": "1",
"help": "incorrect authentication delay", "help": "incorrect authentication delay",
"type": positive_float}), "type": positive_float}),
("strip_domain", {
"value": "False",
"help": "strip domain from username",
"type": bool}),
("lc_username", { ("lc_username", {
"value": "False", "value": "False",
"help": "convert username to lowercase, must be true for case-insensitive auth providers", "help": "convert username to lowercase, must be true for case-insensitive auth providers",

View file

@ -3,14 +3,23 @@ from enum import Enum
from typing import Sequence from typing import Sequence
from radicale import pathutils, utils from radicale import pathutils, utils
from radicale.log import logger
INTERNAL_TYPES: Sequence[str] = ("none", "rabbitmq") INTERNAL_TYPES: Sequence[str] = ("none", "rabbitmq")
def load(configuration): def load(configuration):
"""Load the storage module chosen in configuration.""" """Load the storage module chosen in configuration."""
return utils.load_plugin( try:
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration) return utils.load_plugin(
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
except Exception as e:
logger.warn(e)
logger.warn("Hook \"%s\" failed to load, falling back to \"none\"." % configuration.get("hook", "type"))
configuration = configuration.copy()
configuration.update({"hook": {"type": "none"}}, "hook", privileged=True)
return utils.load_plugin(
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
class BaseHook: class BaseHook:

View file

@ -49,6 +49,12 @@ def read_components(s: str) -> List[vobject.base.Component]:
s = re.sub(r"^(PHOTO(?:;[^:\r\n]*)?;ENCODING=b(?:;[^:\r\n]*)?:)" s = re.sub(r"^(PHOTO(?:;[^:\r\n]*)?;ENCODING=b(?:;[^:\r\n]*)?:)"
r"data:[^;,\r\n]*;base64,", r"\1", s, r"data:[^;,\r\n]*;base64,", r"\1", s,
flags=re.MULTILINE | re.IGNORECASE) flags=re.MULTILINE | re.IGNORECASE)
# Workaround for bug with malformed ICS files containing control codes
# Filter out all control codes except those we expect to find:
# * 0x09 Horizontal Tab
# * 0x0A Line Feed
# * 0x0D Carriage Return
s = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', s)
return list(vobject.readComponents(s, allowQP=True)) return list(vobject.readComponents(s, allowQP=True))
@ -298,7 +304,7 @@ def find_time_range(vobject_item: vobject.base.Component, tag: str
Returns a tuple (``start``, ``end``) where ``start`` and ``end`` are Returns a tuple (``start``, ``end``) where ``start`` and ``end`` are
POSIX timestamps. POSIX timestamps.
This is intened to be used for matching against simplified prefilters. This is intended to be used for matching against simplified prefilters.
""" """
if not tag: if not tag:

View file

@ -241,7 +241,7 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
""" """
# HACK: According to rfc5545-3.8.4.4 an recurrance that is resheduled # HACK: According to rfc5545-3.8.4.4 a recurrence that is rescheduled
# with Recurrence ID affects the recurrence itself and all following # with Recurrence ID affects the recurrence itself and all following
# recurrences too. This is not respected and client don't seem to bother # recurrences too. This is not respected and client don't seem to bother
# either. # either.

View file

@ -22,7 +22,7 @@ config (section "rights", key "file").
The login is matched against the "user" key, and the collection path The login is matched against the "user" key, and the collection path
is matched against the "collection" key. In the "collection" regex you can use is matched against the "collection" key. In the "collection" regex you can use
`{user}` and get groups from the "user" regex with `{0}`, `{1}`, etc. `{user}` and get groups from the "user" regex with `{0}`, `{1}`, etc.
In consequence of the parameter subsitution you have to write `{{` and `}}` In consequence of the parameter substitution you have to write `{{` and `}}`
if you want to use regular curly braces in the "user" and "collection" regexes. if you want to use regular curly braces in the "user" and "collection" regexes.
For example, for the "user" key, ".+" means "authenticated user" and ".*" For example, for the "user" key, ".+" means "authenticated user" and ".*"

View file

@ -291,7 +291,7 @@ def serve(configuration: config.Configuration,
try: try:
getaddrinfo = socket.getaddrinfo(address_port[0], address_port[1], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP) getaddrinfo = socket.getaddrinfo(address_port[0], address_port[1], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
except OSError as e: except OSError as e:
logger.warn("cannot retrieve IPv4 or IPv6 address of '%s': %s" % (format_address(address_port), e)) logger.warning("cannot retrieve IPv4 or IPv6 address of '%s': %s" % (format_address(address_port), e))
continue continue
logger.debug("getaddrinfo of '%s': %s" % (format_address(address_port), getaddrinfo)) logger.debug("getaddrinfo of '%s': %s" % (format_address(address_port), getaddrinfo))
for (address_family, socket_kind, socket_proto, socket_flags, socket_address) in getaddrinfo: for (address_family, socket_kind, socket_proto, socket_flags, socket_address) in getaddrinfo:
@ -299,7 +299,7 @@ def serve(configuration: config.Configuration,
try: try:
server = server_class(configuration, address_family, (socket_address[0], socket_address[1]), RequestHandler) server = server_class(configuration, address_family, (socket_address[0], socket_address[1]), RequestHandler)
except OSError as e: except OSError as e:
logger.warn("cannot create server socket on '%s': %s" % (format_address(socket_address), e)) logger.warning("cannot create server socket on '%s': %s" % (format_address(socket_address), e))
continue continue
servers[server.socket] = server servers[server.socket] = server
server.set_app(application) server.set_app(application)

View file

@ -84,7 +84,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
cache_content = self._load_item_cache(href, cache_hash) cache_content = self._load_item_cache(href, cache_hash)
if cache_content is None: if cache_content is None:
with self._acquire_cache_lock("item"): with self._acquire_cache_lock("item"):
# Lock the item cache to prevent multpile processes from # Lock the item cache to prevent multiple processes from
# generating the same data in parallel. # generating the same data in parallel.
# This improves the performance for multiple requests. # This improves the performance for multiple requests.
if self._storage._lock.locked == "r": if self._storage._lock.locked == "r":
@ -127,7 +127,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
def get_multi(self, hrefs: Iterable[str] def get_multi(self, hrefs: Iterable[str]
) -> Iterator[Tuple[str, Optional[radicale_item.Item]]]: ) -> Iterator[Tuple[str, Optional[radicale_item.Item]]]:
# It's faster to check for file name collissions here, because # It's faster to check for file name collisions here, because
# we only need to call os.listdir once. # we only need to call os.listdir once.
files = None files = None
for href in hrefs: for href in hrefs:
@ -146,7 +146,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
def get_all(self) -> Iterator[radicale_item.Item]: def get_all(self) -> Iterator[radicale_item.Item]:
for href in self._list(): for href in self._list():
# We don't need to check for collissions, because the file names # We don't need to check for collisions, because the file names
# are from os.listdir. # are from os.listdir.
item = self._get(href, verify_href=False) item = self._get(href, verify_href=False)
if item is not None: if item is not None:

View file

@ -112,7 +112,7 @@ class BaseTest:
for response in xml.findall(xmlutils.make_clark("D:response")): for response in xml.findall(xmlutils.make_clark("D:response")):
href = response.find(xmlutils.make_clark("D:href")) href = response.find(xmlutils.make_clark("D:href"))
assert href.text not in path_responses assert href.text not in path_responses
prop_respones: Dict[str, Tuple[int, ET.Element]] = {} prop_responses: Dict[str, Tuple[int, ET.Element]] = {}
for propstat in response.findall( for propstat in response.findall(
xmlutils.make_clark("D:propstat")): xmlutils.make_clark("D:propstat")):
status = propstat.find(xmlutils.make_clark("D:status")) status = propstat.find(xmlutils.make_clark("D:status"))
@ -121,16 +121,16 @@ class BaseTest:
for element in propstat.findall( for element in propstat.findall(
"./%s/*" % xmlutils.make_clark("D:prop")): "./%s/*" % xmlutils.make_clark("D:prop")):
human_tag = xmlutils.make_human_tag(element.tag) human_tag = xmlutils.make_human_tag(element.tag)
assert human_tag not in prop_respones assert human_tag not in prop_responses
prop_respones[human_tag] = (status_code, element) prop_responses[human_tag] = (status_code, element)
status = response.find(xmlutils.make_clark("D:status")) status = response.find(xmlutils.make_clark("D:status"))
if status is not None: if status is not None:
assert not prop_respones assert not prop_responses
assert status.text.startswith("HTTP/1.1 ") assert status.text.startswith("HTTP/1.1 ")
status_code = int(status.text.split(" ")[1]) status_code = int(status.text.split(" ")[1])
path_responses[href.text] = status_code path_responses[href.text] = status_code
else: else:
path_responses[href.text] = prop_respones path_responses[href.text] = prop_responses
return path_responses return path_responses
@staticmethod @staticmethod

View file

@ -115,6 +115,16 @@ class TestBaseAuthRequests(BaseTest):
def test_htpasswd_comment(self) -> None: def test_htpasswd_comment(self) -> None:
self._test_htpasswd("plain", "#comment\n #comment\n \ntmp:bepo\n\n") self._test_htpasswd("plain", "#comment\n #comment\n \ntmp:bepo\n\n")
def test_htpasswd_lc_username(self) -> None:
self.configure({"auth": {"lc_username": "True"}})
self._test_htpasswd("plain", "tmp:bepo", (
("tmp", "bepo", True), ("TMP", "bepo", True), ("tmp1", "bepo", False)))
def test_htpasswd_strip_domain(self) -> None:
self.configure({"auth": {"strip_domain": "True"}})
self._test_htpasswd("plain", "tmp:bepo", (
("tmp", "bepo", True), ("tmp@domain.example", "bepo", True), ("tmp1", "bepo", False)))
def test_remote_user(self) -> None: def test_remote_user(self) -> None:
self.configure({"auth": {"type": "remote_user"}}) self.configure({"auth": {"type": "remote_user"}})
_, responses = self.propfind("/", """\ _, responses = self.propfind("/", """\

View file

@ -360,7 +360,7 @@ permissions: RrWw""")
self.get(path1, check=404) self.get(path1, check=404)
self.get(path2) self.get(path2)
def test_move_between_colections(self) -> None: def test_move_between_collections(self) -> None:
"""Move a item.""" """Move a item."""
self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/") self.mkcalendar("/calendar2.ics/")
@ -373,7 +373,7 @@ permissions: RrWw""")
self.get(path1, check=404) self.get(path1, check=404)
self.get(path2) self.get(path2)
def test_move_between_colections_duplicate_uid(self) -> None: def test_move_between_collections_duplicate_uid(self) -> None:
"""Move a item to a collection which already contains the UID.""" """Move a item to a collection which already contains the UID."""
self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/") self.mkcalendar("/calendar2.ics/")
@ -389,7 +389,7 @@ permissions: RrWw""")
assert xml.tag == xmlutils.make_clark("D:error") assert xml.tag == xmlutils.make_clark("D:error")
assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
def test_move_between_colections_overwrite(self) -> None: def test_move_between_collections_overwrite(self) -> None:
"""Move a item to a collection which already contains the item.""" """Move a item to a collection which already contains the item."""
self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/") self.mkcalendar("/calendar2.ics/")
@ -403,8 +403,8 @@ permissions: RrWw""")
self.request("MOVE", path1, check=204, HTTP_OVERWRITE="T", self.request("MOVE", path1, check=204, HTTP_OVERWRITE="T",
HTTP_DESTINATION="http://127.0.0.1/"+path2) HTTP_DESTINATION="http://127.0.0.1/"+path2)
def test_move_between_colections_overwrite_uid_conflict(self) -> None: def test_move_between_collections_overwrite_uid_conflict(self) -> None:
"""Move a item to a collection which already contains the item with """Move an item to a collection which already contains the item with
a different UID.""" a different UID."""
self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar1.ics/")
self.mkcalendar("/calendar2.ics/") self.mkcalendar("/calendar2.ics/")

View file

@ -1,26 +1,31 @@
[tool:pytest] [tool:pytest]
addopts = --typeguard-packages=radicale
[tox:tox] [tox:tox]
min_version = 4.0
envlist = py, flake8, isort, mypy
[testenv] [testenv]
extras = test extras =
test
deps = deps =
flake8 pytest
isort
# mypy installation fails with pypy<3.9
mypy; implementation_name!='pypy' or python_version>='3.9'
types-setuptools
pytest-cov pytest-cov
commands = commands = pytest -r s --cov --cov-report=term --cov-report=xml .
flake8 .
isort --check --diff . [testenv:flake8]
# Run mypy if it's installed deps = flake8==7.1.0
python -c 'import importlib.util, subprocess, sys; \ commands = flake8 .
importlib.util.find_spec("mypy") \ skip_install = True
and sys.exit(subprocess.run(["mypy", "."]).returncode) \
or print("Skipped: mypy is not installed")' [testenv:isort]
pytest -r s --cov --cov-report=term --cov-report=xml . deps = isort==5.13.2
commands = isort --check --diff .
skip_install = True
[testenv:mypy]
deps = mypy==1.11.0
commands = mypy .
skip_install = True
[tool:isort] [tool:isort]
known_standard_library = _dummy_thread,_thread,abc,aifc,argparse,array,ast,asynchat,asyncio,asyncore,atexit,audioop,base64,bdb,binascii,binhex,bisect,builtins,bz2,cProfile,calendar,cgi,cgitb,chunk,cmath,cmd,code,codecs,codeop,collections,colorsys,compileall,concurrent,configparser,contextlib,contextvars,copy,copyreg,crypt,csv,ctypes,curses,dataclasses,datetime,dbm,decimal,difflib,dis,distutils,doctest,dummy_threading,email,encodings,ensurepip,enum,errno,faulthandler,fcntl,filecmp,fileinput,fnmatch,formatter,fpectl,fractions,ftplib,functools,gc,getopt,getpass,gettext,glob,grp,gzip,hashlib,heapq,hmac,html,http,imaplib,imghdr,imp,importlib,inspect,io,ipaddress,itertools,json,keyword,lib2to3,linecache,locale,logging,lzma,macpath,mailbox,mailcap,marshal,math,mimetypes,mmap,modulefinder,msilib,msvcrt,multiprocessing,netrc,nis,nntplib,ntpath,numbers,operator,optparse,os,ossaudiodev,parser,pathlib,pdb,pickle,pickletools,pipes,pkgutil,platform,plistlib,poplib,posix,posixpath,pprint,profile,pstats,pty,pwd,py_compile,pyclbr,pydoc,queue,quopri,random,re,readline,reprlib,resource,rlcompleter,runpy,sched,secrets,select,selectors,shelve,shlex,shutil,signal,site,smtpd,smtplib,sndhdr,socket,socketserver,spwd,sqlite3,sre,sre_compile,sre_constants,sre_parse,ssl,stat,statistics,string,stringprep,struct,subprocess,sunau,symbol,symtable,sys,sysconfig,syslog,tabnanny,tarfile,telnetlib,tempfile,termios,test,textwrap,threading,time,timeit,tkinter,token,tokenize,trace,traceback,tracemalloc,tty,turtle,turtledemo,types,typing,unicodedata,unittest,urllib,uu,uuid,venv,warnings,wave,weakref,webbrowser,winreg,winsound,wsgiref,xdrlib,xml,xmlrpc,zipapp,zipfile,zipimport,zlib known_standard_library = _dummy_thread,_thread,abc,aifc,argparse,array,ast,asynchat,asyncio,asyncore,atexit,audioop,base64,bdb,binascii,binhex,bisect,builtins,bz2,cProfile,calendar,cgi,cgitb,chunk,cmath,cmd,code,codecs,codeop,collections,colorsys,compileall,concurrent,configparser,contextlib,contextvars,copy,copyreg,crypt,csv,ctypes,curses,dataclasses,datetime,dbm,decimal,difflib,dis,distutils,doctest,dummy_threading,email,encodings,ensurepip,enum,errno,faulthandler,fcntl,filecmp,fileinput,fnmatch,formatter,fpectl,fractions,ftplib,functools,gc,getopt,getpass,gettext,glob,grp,gzip,hashlib,heapq,hmac,html,http,imaplib,imghdr,imp,importlib,inspect,io,ipaddress,itertools,json,keyword,lib2to3,linecache,locale,logging,lzma,macpath,mailbox,mailcap,marshal,math,mimetypes,mmap,modulefinder,msilib,msvcrt,multiprocessing,netrc,nis,nntplib,ntpath,numbers,operator,optparse,os,ossaudiodev,parser,pathlib,pdb,pickle,pickletools,pipes,pkgutil,platform,plistlib,poplib,posix,posixpath,pprint,profile,pstats,pty,pwd,py_compile,pyclbr,pydoc,queue,quopri,random,re,readline,reprlib,resource,rlcompleter,runpy,sched,secrets,select,selectors,shelve,shlex,shutil,signal,site,smtpd,smtplib,sndhdr,socket,socketserver,spwd,sqlite3,sre,sre_compile,sre_constants,sre_parse,ssl,stat,statistics,string,stringprep,struct,subprocess,sunau,symbol,symtable,sys,sysconfig,syslog,tabnanny,tarfile,telnetlib,tempfile,termios,test,textwrap,threading,time,timeit,tkinter,token,tokenize,trace,traceback,tracemalloc,tty,turtle,turtledemo,types,typing,unicodedata,unittest,urllib,uu,uuid,venv,warnings,wave,weakref,webbrowser,winreg,winsound,wsgiref,xdrlib,xml,xmlrpc,zipapp,zipfile,zipimport,zlib

View file

@ -38,9 +38,9 @@ web_files = ["web/internal_data/css/icon.png",
install_requires = ["defusedxml", "passlib", "vobject>=0.9.6", install_requires = ["defusedxml", "passlib", "vobject>=0.9.6",
"python-dateutil>=2.7.3", "python-dateutil>=2.7.3",
"pika>=1.1.0", "pika>=1.1.0",
"setuptools; python_version<'3.9'"] ]
bcrypt_requires = ["bcrypt"] bcrypt_requires = ["bcrypt"]
test_requires = ["pytest>=7", "typeguard<4.3", "waitress", *bcrypt_requires] test_requires = ["pytest>=7", "waitress", *bcrypt_requires]
setup( setup(
name="Radicale", name="Radicale",
@ -75,6 +75,7 @@ setup(
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Office/Business :: Groupware"]) "Topic :: Office/Business :: Groupware"])