Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial commit for Notes API #137

Merged
merged 5 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 20 additions & 13 deletions .github/workflows/analysis-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,7 @@ jobs:

- name: Install Talk
working-directory: apps/spreed
run: |
make dev-setup
make build-js
run: make dev-setup

- name: Enable Talk
run: php occ app:enable spreed
Expand Down Expand Up @@ -304,9 +302,7 @@ jobs:

- name: Install Talk
working-directory: apps/spreed
run: |
make dev-setup
make build-js
run: make dev-setup

- name: Enable Talk
run: php occ app:enable spreed
Expand Down Expand Up @@ -513,6 +509,13 @@ jobs:
ref: "master"
path: apps/notifications

- name: Checkout Notes
uses: actions/checkout@v4
with:
repository: nextcloud/notes
ref: "main"
path: apps/notes

- name: Set up & run Nextcloud
env:
DB_PORT: 4444
Expand All @@ -524,6 +527,7 @@ jobs:
./occ config:system:set loglevel --value=0 --type=integer
./occ config:system:set debug --value=true --type=boolean
./occ app:enable notifications
./occ app:enable notes
php -S localhost:8080 &

- name: Checkout NcPyApi
Expand Down Expand Up @@ -562,9 +566,7 @@ jobs:

- name: Install Talk
working-directory: apps/spreed
run: |
make dev-setup
make build-js
run: make dev-setup

- name: Enable Talk
run: php occ app:enable spreed
Expand Down Expand Up @@ -692,7 +694,6 @@ jobs:
working-directory: apps/spreed
run: |
make dev-setup
make build-js

- name: Enable Talk
run: php occ app:enable spreed
Expand Down Expand Up @@ -775,6 +776,13 @@ jobs:
ref: ${{ matrix.nextcloud }}
path: apps/activity

- name: Checkout Notes
uses: actions/checkout@v4
with:
repository: nextcloud/notes
ref: "main"
path: apps/notes

- name: Set up & run Nextcloud
env:
DB_PORT: 4444
Expand All @@ -787,6 +795,7 @@ jobs:
./occ config:system:set debug --value=true --type=boolean
./occ app:enable notifications
./occ app:enable activity
./occ app:enable notes
php -S localhost:8080 &

- name: Checkout NcPyApi
Expand Down Expand Up @@ -830,9 +839,7 @@ jobs:

- name: Install Talk
working-directory: apps/spreed
run: |
make dev-setup
make build-js
run: make dev-setup

- name: Enable Talk
run: php occ app:enable spreed
Expand Down
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

All notable changes to this project will be documented in this file.

## [0.3.1 - 2023-09-30]
## [0.3.1 - 2023-10-0x]

### Added

- CalendarAPI with the help of [caldav](https://pypi.org/project/caldav/) package.
- CalendarAPI with the help of [caldav](https://pypi.org/project/caldav/) package. #136
- [NotesAPI](https://github.com/nextcloud/notes) #137

## [0.3.0 - 2023-09-28]

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ Python library that provides a robust and well-documented API that allows develo
| Shares | ✅ | ✅ | ✅ |
| Users & Groups | ✅ | ✅ | ✅ |
| User & Weather status | ✅ | ✅ | ✅ |
| Weather status | ✅ | ✅ | ✅ |
| Other APIs*** | ✅ | ✅ | ✅ |
| Talk Bot API* | N/A | ✅ | ✅ |
| Text Processing* | N/A | ❌ | ❌ |
| SpeechToText* | N/A | ❌ | ❌ |

&ast;_available only for NextcloudApp_<br>
&ast;&ast;&ast;_Activity, Notes_

### Differences between the Nextcloud and NextcloudApp classes

Expand Down
2 changes: 1 addition & 1 deletion benchmarks/aa_overhead_dav_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def measure_download_1mb(nc_obj: Union[Nextcloud, NextcloudApp]) -> [Any, float]
start_time = perf_counter()
for _ in range(ITERS):
nc_obj.files.download(small_file_name)
nc_obj._session.init_adapter(restart=not CACHE_SESS) # noqa
nc_obj._session.init_adapter_dav(restart=not CACHE_SESS) # noqa
end_time = perf_counter()
nc_obj.files.delete(small_file_name, not_fail=True)
return __result, round((end_time - start_time) / ITERS, 3)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/aa_overhead_dav_download_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def measure_download_100mb(nc_obj: Union[Nextcloud, NextcloudApp]) -> [Any, floa
for _ in range(ITERS):
medium_file.seek(0)
nc_obj.files.download2stream(medium_file_name, medium_file)
nc_obj._session.init_adapter(restart=not CACHE_SESS) # noqa
nc_obj._session.init_adapter_dav(restart=not CACHE_SESS) # noqa
end_time = perf_counter()
nc_obj.files.delete(medium_file_name, not_fail=True)
return __result, round((end_time - start_time) / ITERS, 3)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/aa_overhead_dav_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def measure_upload_1mb(nc_obj: Union[Nextcloud, NextcloudApp]) -> [Any, float]:
start_time = perf_counter()
for _ in range(ITERS):
nc_obj.files.upload(small_file_name, small_file)
nc_obj._session.init_adapter(restart=not CACHE_SESS) # noqa
nc_obj._session.init_adapter_dav(restart=not CACHE_SESS) # noqa
end_time = perf_counter()
nc_obj.files.delete(small_file_name, not_fail=True)
return __result, round((end_time - start_time) / ITERS, 3)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/aa_overhead_dav_upload_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def measure_upload_100mb(nc_obj: Union[Nextcloud, NextcloudApp]) -> [Any, float]
for _ in range(ITERS):
medium_file.seek(0)
nc_obj.files.upload_stream(medium_file_name, medium_file)
nc_obj._session.init_adapter(restart=not CACHE_SESS) # noqa
nc_obj._session.init_adapter_dav(restart=not CACHE_SESS) # noqa
end_time = perf_counter()
nc_obj.files.delete(medium_file_name, not_fail=True)
return __result, round((end_time - start_time) / ITERS, 3)
Expand Down
13 changes: 13 additions & 0 deletions docs/reference/Notes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.. py:currentmodule:: nc_py_api.notes

Notes API
=========

.. autoclass:: nc_py_api.notes.Note
:members:

.. autoclass:: nc_py_api.notes.NotesSettings
:members:

.. autoclass:: nc_py_api.notes._NotesAPI
:members:
3 changes: 2 additions & 1 deletion docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ Reference
Nextcloud
ExApp
Apps
ActivityApp
Files/index.rst
Users/index.rst
Exceptions
Talk
TalkBot
Calendar
ActivityApp
Notes
Session
67 changes: 50 additions & 17 deletions nc_py_api/_session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Session represents one connection to Nextcloud. All related stuff for these live here."""

import typing
from abc import ABC, abstractmethod
from base64 import b64encode
from collections.abc import Iterator
Expand Down Expand Up @@ -132,6 +132,7 @@ def __init__(self, **kwargs):

class NcSessionBasic(ABC):
adapter: Client
adapter_dav: Client
cfg: BasicConfig
user: str
custom_headers: dict
Expand All @@ -145,11 +146,14 @@ def __init__(self, **kwargs):
self.custom_headers = kwargs.get("headers", {})
self.limits = Limits(max_keepalive_connections=20, max_connections=20, keepalive_expiry=60.0)
self.init_adapter()
self.init_adapter_dav()
self.response_headers = HttpxHeaders()

def __del__(self):
if hasattr(self, "adapter") and self.adapter:
self.adapter.close()
if hasattr(self, "adapter_dav") and self.adapter_dav:
self.adapter_dav.close()

def get_stream(self, path: str, params: Optional[dict] = None, **kwargs) -> Iterator[Response]:
headers = kwargs.pop("headers", {})
Expand All @@ -164,6 +168,24 @@ def _get_stream(self, path_params: str, headers: dict, **kwargs) -> Iterator[Res
"GET", f"{self.cfg.endpoint}{path_params}", headers=headers, timeout=timeout, **kwargs
)

def request_json(
self,
method: str,
path: str,
params: Optional[dict] = None,
data: Optional[Union[bytes, str]] = None,
json: Optional[Union[dict, list]] = None,
**kwargs,
) -> dict:
method = method.upper()
if params is None:
params = {}
params.update({"format": "json"})
headers = kwargs.pop("headers", {})
data_bytes = self.__data_to_bytes(headers, data, json)
r = self._ocs(method, f"{quote(path)}?{urlencode(params, True)}", headers, data_bytes, not_parse=True)
return loads(r.text) if r.status_code != 304 else {}

def ocs(
self,
method: str,
Expand All @@ -178,12 +200,7 @@ def ocs(
params = {}
params.update({"format": "json"})
headers = kwargs.pop("headers", {})
data_bytes = None
if data is not None:
data_bytes = data.encode("UTF-8") if isinstance(data, str) else data
elif json is not None:
headers.update({"Content-Type": "application/json"})
data_bytes = dumps(json).encode("utf-8")
data_bytes = self.__data_to_bytes(headers, data, json)
return self._ocs(method, f"{quote(path)}?{urlencode(params, True)}", headers, data=data_bytes, **kwargs)

def _ocs(self, method: str, path_params: str, headers: dict, data: Optional[bytes], **kwargs):
Expand Down Expand Up @@ -234,12 +251,7 @@ def dav(
**kwargs,
) -> Response:
headers = kwargs.pop("headers", {})
data_bytes = None
if data is not None:
data_bytes = data.encode("UTF-8") if isinstance(data, str) else data
elif json is not None:
headers.update({"Content-Type": "application/json"})
data_bytes = dumps(json).encode("utf-8")
data_bytes = self.__data_to_bytes(headers, data, json)
return self._dav(
method,
quote(self.cfg.dav_url_suffix + path) if isinstance(path, str) else path,
Expand All @@ -258,9 +270,9 @@ def dav_stream(
return self._dav_stream(method, quote(self.cfg.dav_url_suffix + path), headers, data_bytes, **kwargs)

def _dav(self, method: str, path: str, headers: dict, data: Optional[bytes], **kwargs) -> Response:
self.init_adapter()
self.init_adapter_dav()
timeout = kwargs.pop("timeout", self.cfg.options.timeout_dav)
result = self.adapter.request(
result = self.adapter_dav.request(
method,
self.cfg.endpoint + path if isinstance(path, str) else str(path),
headers=headers,
Expand All @@ -272,9 +284,9 @@ def _dav(self, method: str, path: str, headers: dict, data: Optional[bytes], **k
return result

def _dav_stream(self, method: str, path: str, headers: dict, data: Optional[bytes], **kwargs) -> Iterator[Response]:
self.init_adapter()
self.init_adapter_dav()
timeout = kwargs.pop("timeout", self.cfg.options.timeout_dav)
return self.adapter.stream(
return self.adapter_dav.stream(
method, self.cfg.endpoint + path, headers=headers, content=data, timeout=timeout, **kwargs
)

Expand All @@ -290,6 +302,16 @@ def init_adapter(self, restart=False) -> None:
self.adapter.cookies.set("XDEBUG_SESSION", options.XDEBUG_SESSION)
self._capabilities = {}

def init_adapter_dav(self, restart=False) -> None:
if getattr(self, "adapter_dav", None) is None or restart:
if restart and hasattr(self, "adapter"):
self.adapter.close()
self.adapter_dav = self._create_adapter()
if self.custom_headers:
self.adapter_dav.headers.update(self.custom_headers)
if options.XDEBUG_SESSION:
self.adapter_dav.cookies.set("XDEBUG_SESSION", options.XDEBUG_SESSION)

@abstractmethod
def _create_adapter(self) -> Client:
pass # pragma: no cover
Expand Down Expand Up @@ -321,6 +343,17 @@ def ae_url(self) -> str:
"""Return base url for the App Ecosystem endpoints."""
return "/ocs/v1.php/apps/app_api/api/v1"

@staticmethod
def __data_to_bytes(
headers: dict, data: Optional[Union[bytes, str]] = None, json: Optional[Union[dict, list]] = None
) -> typing.Optional[bytes]:
if data is not None:
return data.encode("UTF-8") if isinstance(data, str) else data
if json is not None:
headers.update({"Content-Type": "application/json"})
return dumps(json).encode("utf-8")
return None


class NcSession(NcSessionBasic):
cfg: Config
Expand Down
2 changes: 2 additions & 0 deletions nc_py_api/ex_app/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ class ApiScope(enum.IntEnum):
"""Allows to register Talk Bots."""
ACTIVITIES = 110
"""Activity App endpoints."""
NOTES = 120
"""Notes App endpoints"""
5 changes: 5 additions & 0 deletions nc_py_api/nextcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .ex_app.defs import ApiScope, LogLvl
from .ex_app.ui.ui import UiApi
from .files.files import FilesAPI
from .notes import _NotesAPI
from .notifications import _NotificationsAPI
from .user_status import _UserStatusAPI
from .users import _UsersAPI
Expand All @@ -36,6 +37,8 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
"""Nextcloud API for File System and Files Sharing"""
preferences: PreferencesAPI
"""Nextcloud User Preferences API"""
notes: _NotesAPI
"""Nextcloud Notes API"""
notifications: _NotificationsAPI
"""Nextcloud API for managing user notifications"""
talk: _TalkAPI
Expand All @@ -56,6 +59,7 @@ def _init_api(self, session: NcSessionBasic):
self.cal = _CalendarAPI(session)
self.files = FilesAPI(session)
self.preferences = PreferencesAPI(session)
self.notes = _NotesAPI(session)
self.notifications = _NotificationsAPI(session)
self.talk = _TalkAPI(session)
self.users = _UsersAPI(session)
Expand Down Expand Up @@ -193,6 +197,7 @@ def user(self, value: str):
self.talk.config_sha = ""
self.talk.modified_since = 0
self.activity.last_given = 0
self.notes.last_etag = ""
self._session.update_server_info()

@property
Expand Down
Loading