diff --git a/CHANGELOG.md b/CHANGELOG.md index a0308cdc..c62ae09c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.3.1 - 2023-09-30] + +### Added + +- CalendarAPI with the help of [caldav](https://pypi.org/project/caldav/) package. + ## [0.3.0 - 2023-09-28] ### Added diff --git a/README.md b/README.md index 94274bd8..ef5d9ee3 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Python library that provides a robust and well-documented API that allows develo ### Capabilities | **_Capability_** | Nextcloud 26 | Nextcloud 27 | Nextcloud 28 | |-----------------------|:------------:|:------------:|:------------:| +| Calendar | ✅ | ✅ | ✅ | | File System & Tags | ✅ | ✅ | ✅ | | Nextcloud Talk** | ✅ | ✅ | ✅ | | Notifications | ✅ | ✅ | ✅ | @@ -37,7 +38,7 @@ Python library that provides a robust and well-documented API that allows develo | SpeechToText* | N/A | ❌ | ❌ | *_available only for NextcloudApp_
-** _work is in progress, not all API's is described, yet._ +** _work is in progress, not all APIs are described yet._ ### Differences between the Nextcloud and NextcloudApp classes diff --git a/docs/Installation.rst b/docs/Installation.rst index 74307681..8a704764 100644 --- a/docs/Installation.rst +++ b/docs/Installation.rst @@ -13,6 +13,10 @@ To use in the Nextcloud Application mode install it with additional ``app`` depe python -m pip install --upgrade "nc_py_api[app]" +To use **Calendar API** just add **calendar** dependency, and command will look like this :command:`pip`:: + + python -m pip install --upgrade "nc_py_api[app,calendar]" + To join the development of **nc_py_api** api install development dependencies with :command:`pip`:: python -m pip install --upgrade "nc_py_api[dev]" diff --git a/docs/reference/Calendar.rst b/docs/reference/Calendar.rst new file mode 100644 index 00000000..adb637ac --- /dev/null +++ b/docs/reference/Calendar.rst @@ -0,0 +1,22 @@ +.. py:currentmodule:: nc_py_api.calendar + +Calendar API +============ + +.. note:: To make this API work you should install **nc_py_api** with **calendar** extra dependency. + +.. code-block:: python + + principal = nc.cal.principal() + calendars = principal.calendars() # get list of calendars + +``nc.cal`` is usual ``caldav.DAVClient`` object with the same API. + +Documentation for ``caldav`` can be found here: `CalDAV <"https://caldav.readthedocs.io/en/latest">`_ + +.. class:: _CalendarAPI + + Class that encapsulates ``caldav.DAVClient``. Avalaible as **cal** in the Nextcloud class. + + .. note:: You should not call ``close`` or ``request`` methods of CalendarAPI, they will be removed somewhere + in the future when ``caldav.DAVClient`` will be rewritten(API compatability will remains). diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 24961d55..2f90bcf6 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -13,4 +13,5 @@ Reference Exceptions Talk TalkBot + Calendar Session diff --git a/nc_py_api/_session.py b/nc_py_api/_session.py index 642aa4ee..a5a7a3b0 100644 --- a/nc_py_api/_session.py +++ b/nc_py_api/_session.py @@ -240,7 +240,13 @@ def dav( elif json is not None: headers.update({"Content-Type": "application/json"}) data_bytes = dumps(json).encode("utf-8") - return self._dav(method, quote(self.cfg.dav_url_suffix + path), headers, data_bytes, **kwargs) + return self._dav( + method, + quote(self.cfg.dav_url_suffix + path) if isinstance(path, str) else path, + headers, + data_bytes, + **kwargs, + ) def dav_stream( self, method: str, path: str, data: Optional[Union[str, bytes]] = None, **kwargs @@ -255,7 +261,12 @@ def _dav(self, method: str, path: str, headers: dict, data: Optional[bytes], **k self.init_adapter() timeout = kwargs.pop("timeout", self.cfg.options.timeout_dav) result = self.adapter.request( - method, self.cfg.endpoint + path, headers=headers, content=data, timeout=timeout, **kwargs + method, + self.cfg.endpoint + path if isinstance(path, str) else str(path), + headers=headers, + content=data, + timeout=timeout, + **kwargs, ) self.response_headers = result.headers return result diff --git a/nc_py_api/_version.py b/nc_py_api/_version.py index 351f311a..0faeaada 100644 --- a/nc_py_api/_version.py +++ b/nc_py_api/_version.py @@ -1,3 +1,3 @@ """Version of nc_py_api.""" -__version__ = "0.3.0" +__version__ = "0.3.1.dev0" diff --git a/nc_py_api/calendar.py b/nc_py_api/calendar.py new file mode 100644 index 00000000..d789869d --- /dev/null +++ b/nc_py_api/calendar.py @@ -0,0 +1,39 @@ +"""Nextcloud Calendar DAV API.""" + +from ._session import NcSessionBasic + +try: + from caldav.davclient import DAVClient, DAVResponse + + class _CalendarAPI(DAVClient): + """Class that encapsulates ``caldav.DAVClient`` to work with the Nextcloud calendar.""" + + def __init__(self, session: NcSessionBasic): + self._session = session + super().__init__(session.cfg.dav_endpoint) + + @property + def available(self) -> bool: + """Returns True if ``caldav`` package is avalaible, False otherwise.""" + return True + + def request(self, url, method="GET", body="", headers={}): # noqa pylint: disable=dangerous-default-value + if isinstance(body, str): + body = body.encode("UTF-8") + if body: + body = body.replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n") + r = self._session.dav(method, url, data=body, headers=headers) + return DAVResponse(r) + +except ImportError: + + class _CalendarAPI: # type: ignore + """A stub class in case **caldav** is missing.""" + + def __init__(self, session: NcSessionBasic): + self._session = session + + @property + def available(self) -> bool: + """Returns True if ``caldav`` package is avalaible, False otherwise.""" + return False diff --git a/nc_py_api/nextcloud.py b/nc_py_api/nextcloud.py index 6cf07048..85bc15f1 100644 --- a/nc_py_api/nextcloud.py +++ b/nc_py_api/nextcloud.py @@ -14,6 +14,7 @@ from ._theming import ThemingInfo, get_parsed_theme from .activity import _ActivityAPI from .apps import _AppsAPI +from .calendar import _CalendarAPI from .ex_app.defs import ApiScope, LogLvl from .ex_app.ui.ui import UiApi from .files.files import FilesAPI @@ -29,6 +30,8 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes """Nextcloud API for App management""" activity: _ActivityAPI """Activity Application API""" + cal: _CalendarAPI + """Nextcloud Calendar API""" files: FilesAPI """Nextcloud API for File System and Files Sharing""" preferences: PreferencesAPI @@ -50,6 +53,7 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes def _init_api(self, session: NcSessionBasic): self.apps = _AppsAPI(session) self.activity = _ActivityAPI(session) + self.cal = _CalendarAPI(session) self.files = FilesAPI(session) self.preferences = PreferencesAPI(session) self.notifications = _NotificationsAPI(session) diff --git a/pyproject.toml b/pyproject.toml index bb3f8e99..df5920ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,9 +60,12 @@ bench = [ "numpy", "py-cpuinfo", ] +calendar = [ + "caldav==1.3.6", +] dev = [ "coverage", - "nc_py_api[bench]", + "nc_py_api[bench,calendar]", "pillow", "pre-commit", "pylint", @@ -71,7 +74,7 @@ dev = [ ] docs = [ "autodoc_pydantic>=2.0.1", - "nc_py_api[app]", + "nc_py_api[app,calendar]", "sphinx>=6.2", "sphinx-copybutton", "sphinx-inline-tabs", diff --git a/tests/actual_tests/calendar_test.py b/tests/actual_tests/calendar_test.py new file mode 100644 index 00000000..dcb55221 --- /dev/null +++ b/tests/actual_tests/calendar_test.py @@ -0,0 +1,23 @@ +import datetime + +import pytest + + +def test_create_delete(nc_client): + if nc_client.cal.available is False: + pytest.skip("``caldav`` package is not installed") + + principal = nc_client.cal.principal() + calendars = principal.calendars() + assert calendars + calendar = calendars[0] + all_events_before = calendar.events() + event = calendar.save_event( + dtstart=datetime.datetime.now(), + dtend=datetime.datetime.now() + datetime.timedelta(hours=1), + summary="NcPyApi + CalDAV test", + ) + all_events_after = calendar.events() + assert len(all_events_after) == len(all_events_before) + 1 + event.delete() + assert len(calendar.events()) == len(all_events_before)