Skip to content

Commit 19ed1e4

Browse files
Drop sync API for Activity, Notes, User Status, Weather Status (#405)
## Summary - Remove sync wrapper classes (`_ActivityAPI`, `_NotesAPI`, `_UserStatusAPI`, `_WeatherStatusAPI`) from 4 module files - Remove sync registrations and state resets from `_NextcloudBasic` / `NextcloudApp` - Remove all corresponding sync tests (68 test cases); async tests fully cover the same functionality - Fix `test_get_conversations_include_status_async` in talk_test to use `AsyncNextcloud` instead of sync `Nextcloud` for user_status setup - Bump version to **0.30.0** - Add async-first deprecation notice to README ## Motivation Moving nc_py_api toward async-only. This is the first batch of sync removals, targeting modules where async tests already mirror every sync test 1:1. ## Test plan - [x] Pre-commit passes (isort, black, ruff, pylint) - [x] Full test suite: 538 passed, 6 skipped (same skips as before) - [x] `grep -r '_NotesAPI\|_WeatherStatusAPI\|_UserStatusAPI\|_ActivityAPI'` returns zero hits - [x] No remaining sync references to removed modules in tests <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Library is async-first with a full asynchronous API; synchronous wrappers are being phased out. * **Documentation** * Deprecation notice for the sync API effective v0.30.0 and migration guidance to async equivalents. * **Refactor** * Sync implementations for Activity, Notes, User Status and Weather Status removed; async counterparts remain. * **Tests** * Many sync tests removed or replaced with async variants. * **Bug Fixes** * Safer handling for missing talk-bot fields and early-exit for non-message objects. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Co-authored-by: Oleksandr Piskun <oleksandr2088@icloud.com>
1 parent fb12595 commit 19ed1e4

17 files changed

Lines changed: 44 additions & 714 deletions

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ Python library that provides a robust and well-documented API that allows develo
1717
* **Reliable**: Minimum number of incompatible changes.
1818
* **Robust**: All code is covered with tests as much as possible.
1919
* **Easy**: Designed to be easy to use.
20-
* **Sync + Async**: Provides both sync and async APIs.
20+
* **Async-first**: Full async API with sync wrappers available for most modules.
21+
22+
### Deprecation notice: sync API
23+
24+
Starting with version **0.30.0**, we are gradually removing sync wrappers in favour of
25+
the async API. The following modules have already lost their sync counterparts:
26+
**Activity**, **Notes**, **User Status**, and **Weather Status**.
27+
28+
All remaining sync methods will be phased out in future releases. If you are still
29+
using the sync `Nextcloud` / `NextcloudApp` classes, we recommend migrating to
30+
`AsyncNextcloud` / `AsyncNextcloudApp` as soon as possible.
2131

2232
### Differences between the Nextcloud and NextcloudApp classes
2333

nc_py_api/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version of nc_py_api."""
22

3-
__version__ = "0.24.2"
3+
__version__ = "0.30.0.dev0"

nc_py_api/activity.py

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from ._exceptions import NextcloudExceptionNotModified
88
from ._misc import check_capabilities, nc_iso_time_to_datetime
9-
from ._session import AsyncNcSessionBasic, NcSessionBasic
9+
from ._session import AsyncNcSessionBasic
1010

1111

1212
@dataclasses.dataclass
@@ -136,58 +136,6 @@ def __repr__(self):
136136
)
137137

138138

139-
class _ActivityAPI:
140-
"""The class provides the Activity Application API."""
141-
142-
_ep_base: str = "/ocs/v1.php/apps/activity"
143-
last_given: int
144-
"""Used by ``get_activities``, when **since** param is ``True``."""
145-
146-
def __init__(self, session: NcSessionBasic):
147-
self._session = session
148-
self.last_given = 0
149-
150-
@property
151-
def available(self) -> bool:
152-
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
153-
return not check_capabilities("activity.apiv2", self._session.capabilities)
154-
155-
def get_activities(
156-
self,
157-
filter_id: ActivityFilter | str = "",
158-
since: int | bool = 0,
159-
limit: int = 50,
160-
object_type: str = "",
161-
object_id: int = 0,
162-
sort: str = "desc",
163-
) -> list[Activity]:
164-
"""Returns activities for the current user.
165-
166-
:param filter_id: Filter to apply, if needed.
167-
:param since: Last activity ID you have seen. When specified, only activities after provided are returned.
168-
Can be set to ``True`` to automatically use last ``last_given`` from previous calls. Default = **0**.
169-
:param limit: Max number of activities to be returned.
170-
:param object_type: Filter the activities to a given object.
171-
:param object_id: Filter the activities to a given object.
172-
:param sort: Sort activities ascending or descending. Default is ``desc``.
173-
174-
.. note:: ``object_type`` and ``object_id`` should only appear together with ``filter_id`` unset.
175-
"""
176-
if since is True:
177-
since = self.last_given
178-
url, params = _get_activities(filter_id, since, limit, object_type, object_id, sort)
179-
try:
180-
result = self._session.ocs("GET", self._ep_base + url, params=params)
181-
except NextcloudExceptionNotModified:
182-
return []
183-
self.last_given = int(self._session.response_headers["X-Activity-Last-Given"])
184-
return [Activity(i) for i in result]
185-
186-
def get_filters(self) -> list[ActivityFilter]:
187-
"""Returns avalaible activity filters."""
188-
return [ActivityFilter(i) for i in self._session.ocs("GET", self._ep_base + "/api/v2/activity/filters")]
189-
190-
191139
class _AsyncActivityAPI:
192140
"""The class provides the async Activity Application API."""
193141

nc_py_api/nextcloud.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
)
2828
from ._talk_api import _AsyncTalkAPI, _TalkAPI
2929
from ._theming import ThemingInfo, get_parsed_theme
30-
from .activity import _ActivityAPI, _AsyncActivityAPI
30+
from .activity import _AsyncActivityAPI
3131
from .apps import _AppsAPI, _AsyncAppsAPI
3232
from .calendar_api import _CalendarAPI
3333
from .ex_app.defs import LogLvl
@@ -37,28 +37,24 @@
3737
from .files.files import FilesAPI
3838
from .files.files_async import AsyncFilesAPI
3939
from .loginflow_v2 import _AsyncLoginFlowV2API, _LoginFlowV2API
40-
from .notes import _AsyncNotesAPI, _NotesAPI
40+
from .notes import _AsyncNotesAPI
4141
from .notifications import _AsyncNotificationsAPI, _NotificationsAPI
42-
from .user_status import _AsyncUserStatusAPI, _UserStatusAPI
42+
from .user_status import _AsyncUserStatusAPI
4343
from .users import _AsyncUsersAPI, _UsersAPI
4444
from .users_groups import _AsyncUsersGroupsAPI, _UsersGroupsAPI
45-
from .weather_status import _AsyncWeatherStatusAPI, _WeatherStatusAPI
45+
from .weather_status import _AsyncWeatherStatusAPI
4646
from .webhooks import _AsyncWebhooksAPI, _WebhooksAPI
4747

4848

4949
class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
5050
apps: _AppsAPI
5151
"""Nextcloud API for App management"""
52-
activity: _ActivityAPI
53-
"""Activity Application API"""
5452
cal: _CalendarAPI
5553
"""Nextcloud Calendar API"""
5654
files: FilesAPI
5755
"""Nextcloud API for File System and Files Sharing"""
5856
preferences: PreferencesAPI
5957
"""Nextcloud User Preferences API"""
60-
notes: _NotesAPI
61-
"""Nextcloud Notes API"""
6258
notifications: _NotificationsAPI
6359
"""Nextcloud API for managing user notifications"""
6460
talk: _TalkAPI
@@ -67,27 +63,19 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
6763
"""Nextcloud API for managing users."""
6864
users_groups: _UsersGroupsAPI
6965
"""Nextcloud API for managing user groups."""
70-
user_status: _UserStatusAPI
71-
"""Nextcloud API for managing users statuses"""
72-
weather_status: _WeatherStatusAPI
73-
"""Nextcloud API for managing user weather statuses"""
7466
webhooks: _WebhooksAPI
7567
"""Nextcloud API for managing webhooks"""
7668
_session: NcSessionBasic
7769

7870
def __init__(self, session: NcSessionBasic):
7971
self.apps = _AppsAPI(session)
80-
self.activity = _ActivityAPI(session)
8172
self.cal = _CalendarAPI(session)
8273
self.files = FilesAPI(session)
8374
self.preferences = PreferencesAPI(session)
84-
self.notes = _NotesAPI(session)
8575
self.notifications = _NotificationsAPI(session)
8676
self.talk = _TalkAPI(session)
8777
self.users = _UsersAPI(session)
8878
self.users_groups = _UsersGroupsAPI(session)
89-
self.user_status = _UserStatusAPI(session)
90-
self.weather_status = _WeatherStatusAPI(session)
9179
self.webhooks = _WebhooksAPI(session)
9280

9381
@property
@@ -381,8 +369,6 @@ def set_user(self, user_id: str):
381369
self._session.set_user(user_id)
382370
self.talk.config_sha = ""
383371
self.talk.modified_since = 0
384-
self.activity.last_given = 0
385-
self.notes.last_etag = ""
386372
self._session.update_server_info()
387373

388374
@property

nc_py_api/notes.py

Lines changed: 1 addition & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from ._exceptions import check_error
1111
from ._misc import check_capabilities, clear_from_params_empty, require_capabilities
12-
from ._session import AsyncNcSessionBasic, NcSessionBasic
12+
from ._session import AsyncNcSessionBasic
1313

1414

1515
@dataclasses.dataclass
@@ -91,144 +91,6 @@ class NotesSettings(typing.TypedDict):
9191
"""Newly created note's files will have this file suffix. Default is **.txt**."""
9292

9393

94-
class _NotesAPI:
95-
"""Class implementing Nextcloud Notes API."""
96-
97-
_ep_base: str = "/index.php/apps/notes/api/v1" # without `index.php` we will get 405 error.
98-
last_etag: str
99-
"""Used by ``get_list``, when **etag** param is ``True``."""
100-
101-
def __init__(self, session: NcSessionBasic):
102-
self._session = session
103-
self.last_etag = ""
104-
105-
@property
106-
def available(self) -> bool:
107-
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
108-
return not check_capabilities("notes", self._session.capabilities)
109-
110-
def get_list(
111-
self,
112-
category: str | None = None,
113-
modified_since: int | None = None,
114-
limit: int | None = None,
115-
cursor: str | None = None,
116-
no_content: bool = False,
117-
etag: bool = False,
118-
) -> list[Note]:
119-
"""Get information of all Notes.
120-
121-
:param category: Filter the result by category name. Notes with another category are not included in the result.
122-
:param modified_since: When provided only results newer than given Unix timestamp are returned.
123-
:param limit: Limit response to contain no more than the given number of notes.
124-
If there are more notes, then the result is chunked and the HTTP response header
125-
**X-Notes-Chunk-Cursor** is sent with a string value.
126-
127-
.. note:: Use :py:attr:`~nc_py_api.nextcloud.Nextcloud.response_headers` property to achieve that.
128-
:param cursor: You should use the string value from the last request's HTTP response header
129-
``X-Notes-Chunk-Cursor`` in order to get the next chunk of notes.
130-
:param no_content: Flag indicating should ``content`` field be excluded from response.
131-
:param etag: Flag indicating should ``ETag`` from last call be used. Default = **False**.
132-
"""
133-
require_capabilities("notes", self._session.capabilities)
134-
params = {
135-
"category": category,
136-
"pruneBefore": modified_since,
137-
"exclude": "content" if no_content else None,
138-
"chunkSize": limit,
139-
"chunkCursor": cursor,
140-
}
141-
clear_from_params_empty(list(params.keys()), params)
142-
headers = {"If-None-Match": self.last_etag} if self.last_etag and etag else {}
143-
r = _res_to_json(self._session.adapter.get(self._ep_base + "/notes", params=params, headers=headers))
144-
self.last_etag = self._session.response_headers["ETag"]
145-
return [Note(i) for i in r]
146-
147-
def by_id(self, note: Note) -> Note:
148-
"""Get updated information about :py:class:`~nc_py_api.notes.Note`."""
149-
require_capabilities("notes", self._session.capabilities)
150-
r = _res_to_json(
151-
self._session.adapter.get(
152-
self._ep_base + f"/notes/{note.note_id}", headers={"If-None-Match": f'"{note.etag}"'}
153-
)
154-
)
155-
return Note(r) if r else note
156-
157-
def create(
158-
self,
159-
title: str,
160-
content: str | None = None,
161-
category: str | None = None,
162-
favorite: bool | None = None,
163-
last_modified: int | str | datetime.datetime | None = None,
164-
) -> Note:
165-
"""Create new Note."""
166-
require_capabilities("notes", self._session.capabilities)
167-
params = {
168-
"title": title,
169-
"content": content,
170-
"category": category,
171-
"favorite": favorite,
172-
"modified": last_modified,
173-
}
174-
clear_from_params_empty(list(params.keys()), params)
175-
return Note(_res_to_json(self._session.adapter.post(self._ep_base + "/notes", json=params)))
176-
177-
def update(
178-
self,
179-
note: Note,
180-
title: str | None = None,
181-
content: str | None = None,
182-
category: str | None = None,
183-
favorite: bool | None = None,
184-
overwrite: bool = False,
185-
) -> Note:
186-
"""Updates Note.
187-
188-
``overwrite`` specifies should be or not the Note updated even if it was changed on server(has different ETag).
189-
"""
190-
require_capabilities("notes", self._session.capabilities)
191-
headers = {"If-Match": f'"{note.etag}"'} if not overwrite else {}
192-
params = {
193-
"title": title,
194-
"content": content,
195-
"category": category,
196-
"favorite": favorite,
197-
}
198-
clear_from_params_empty(list(params.keys()), params)
199-
if not params:
200-
raise ValueError("Nothing to update.")
201-
return Note(
202-
_res_to_json(
203-
self._session.adapter.put(self._ep_base + f"/notes/{note.note_id}", json=params, headers=headers)
204-
)
205-
)
206-
207-
def delete(self, note: int | Note) -> None:
208-
"""Deletes a Note."""
209-
require_capabilities("notes", self._session.capabilities)
210-
note_id = note.note_id if isinstance(note, Note) else note
211-
check_error(self._session.adapter.delete(self._ep_base + f"/notes/{note_id}"))
212-
213-
def get_settings(self) -> NotesSettings:
214-
"""Returns Notes App settings."""
215-
require_capabilities("notes", self._session.capabilities)
216-
r = _res_to_json(self._session.adapter.get(self._ep_base + "/settings"))
217-
return {"notes_path": r["notesPath"], "file_suffix": r["fileSuffix"]}
218-
219-
def set_settings(self, notes_path: str | None = None, file_suffix: str | None = None) -> None:
220-
"""Change specified setting(s)."""
221-
if notes_path is None and file_suffix is None:
222-
raise ValueError("No setting to change.")
223-
require_capabilities("notes", self._session.capabilities)
224-
params = {
225-
"notesPath": notes_path,
226-
"fileSuffix": file_suffix,
227-
}
228-
clear_from_params_empty(list(params.keys()), params)
229-
check_error(self._session.adapter.put(self._ep_base + "/settings", json=params))
230-
231-
23294
class _AsyncNotesAPI:
23395
"""Class implements Async Nextcloud Notes API."""
23496

nc_py_api/talk_bot.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .nextcloud import AsyncNextcloudApp, NextcloudApp
1616

1717

18-
class ObjectContent(typing.TypedDict):
18+
class ObjectContent(typing.TypedDict, total=False):
1919
"""Object content of :py:class:`~nc_py_api.talk_bot.TalkBotMessage`."""
2020

2121
message: str
@@ -62,13 +62,22 @@ def object_name(self) -> str:
6262

6363
@property
6464
def object_content(self) -> ObjectContent:
65-
"""Dictionary with a ``message`` and ``parameters`` keys."""
66-
return json.loads(self._raw_data["object"]["content"])
65+
"""Dictionary with a ``message`` and ``parameters`` keys.
66+
67+
.. note:: May return an empty dict for system messages that have no content.
68+
"""
69+
content = self._raw_data["object"].get("content")
70+
if content is None:
71+
return {}
72+
return json.loads(content)
6773

6874
@property
6975
def object_media_type(self) -> str:
70-
"""``text/markdown`` when the message should be interpreted as **Markdown**, otherwise ``text/plain``."""
71-
return self._raw_data["object"]["mediaType"]
76+
"""``text/markdown`` when the message should be interpreted as **Markdown**, otherwise ``text/plain``.
77+
78+
.. note:: May return an empty string for system messages.
79+
"""
80+
return self._raw_data["object"].get("mediaType", "")
7281

7382
@property
7483
def conversation_token(self) -> str:

0 commit comments

Comments
 (0)