nc-py-api 0.17.0__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/AUTHORS +1 -0
  2. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/CHANGELOG.md +21 -0
  3. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/PKG-INFO +3 -3
  4. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_session.py +2 -2
  5. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_version.py +1 -1
  6. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/apps.py +0 -13
  7. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/__init__.py +1 -0
  8. nc_py_api-0.18.0/nc_py_api/ex_app/logging.py +46 -0
  9. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/files/__init__.py +8 -2
  10. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/files/files.py +8 -3
  11. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/files/files_async.py +8 -3
  12. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/nextcloud.py +24 -12
  13. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/users.py +2 -2
  14. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/webhooks.py +15 -1
  15. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/pyproject.toml +3 -2
  16. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/.gitignore +0 -0
  17. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/LICENSE.txt +0 -0
  18. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/README.md +0 -0
  19. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/__init__.py +0 -0
  20. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_deffered_error.py +0 -0
  21. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_exceptions.py +0 -0
  22. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_misc.py +0 -0
  23. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_preferences.py +0 -0
  24. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_preferences_ex.py +0 -0
  25. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_talk_api.py +0 -0
  26. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/_theming.py +0 -0
  27. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/activity.py +0 -0
  28. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/calendar.py +0 -0
  29. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/defs.py +0 -0
  30. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/events_listener.py +0 -0
  31. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/integration_fastapi.py +0 -0
  32. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/misc.py +0 -0
  33. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/occ_commands.py +0 -0
  34. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/persist_transformers_cache.py +0 -0
  35. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/providers/__init__.py +0 -0
  36. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/providers/providers.py +0 -0
  37. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/providers/task_processing.py +0 -0
  38. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/ui/__init__.py +0 -0
  39. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/ui/files_actions.py +0 -0
  40. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/ui/resources.py +0 -0
  41. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/ui/settings.py +0 -0
  42. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/ui/top_menu.py +0 -0
  43. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/ui/ui.py +0 -0
  44. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/ex_app/uvicorn_fastapi.py +0 -0
  45. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/files/_files.py +0 -0
  46. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/files/sharing.py +0 -0
  47. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/loginflow_v2.py +0 -0
  48. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/notes.py +0 -0
  49. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/notifications.py +0 -0
  50. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/options.py +0 -0
  51. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/talk.py +0 -0
  52. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/talk_bot.py +0 -0
  53. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/user_status.py +0 -0
  54. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/users_groups.py +0 -0
  55. {nc_py_api-0.17.0 → nc_py_api-0.18.0}/nc_py_api/weather_status.py +0 -0
@@ -6,6 +6,7 @@ answer newbie questions, and generally made NC-Py-API that much better:
6
6
  Alexander Piskun <bigcat88@icloud.com>
7
7
  CooperGerman <https://github.com/CooperGerman>
8
8
  Tobias Tschech <Tobias@tschech-online.de>
9
+ Scott Williams <scottwilliams@ucsb.edu>
9
10
  <Please alphabetize new entries>
10
11
 
11
12
  A big THANK YOU goes to:
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.18.0 - 2024-10-09]
6
+
7
+ ### Added
8
+
9
+ - New `webhooks.unregister_all` method. #309
10
+
11
+ ### Fixed
12
+
13
+ - Files: `user` and `user_path` properties in `FSNode` when Nextcloud located in the sub-path. #297 Thanks to @vwbusguy
14
+ - `files.download_directory_as_zip` method now supports upcoming Nextcloud 31. #304
15
+
16
+ ## [0.17.1 - 2024-09-06]
17
+
18
+ ### Added
19
+
20
+ - NextcloudApp: `setup_nextcloud_logging` function to support transparently sending logs to Nextcloud. #294
21
+
22
+ ### Fixed
23
+
24
+ - NextcloudApp: `nc.log` now suppresses all exceptions to safe call it anywhere(for example in exception handlers). #293
25
+
5
26
  ## [0.17.0 - 2024-09-05]
6
27
 
7
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nc-py-api
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: Nextcloud Python Framework
5
5
  Project-URL: Changelog, https://github.com/cloud-py-api/nc_py_api/blob/main/CHANGELOG.md
6
6
  Project-URL: Documentation, https://cloud-py-api.github.io/nc_py_api/
@@ -70,8 +70,8 @@ Requires-Dist: caldav==1.3.6; extra == 'docs'
70
70
  Requires-Dist: sphinx-copybutton; extra == 'docs'
71
71
  Requires-Dist: sphinx-inline-tabs; extra == 'docs'
72
72
  Requires-Dist: sphinx-issues>=3.0.1; extra == 'docs'
73
- Requires-Dist: sphinx-rtd-theme>=1; extra == 'docs'
74
- Requires-Dist: sphinx>=6.2; extra == 'docs'
73
+ Requires-Dist: sphinx-rtd-theme<3; extra == 'docs'
74
+ Requires-Dist: sphinx<8; extra == 'docs'
75
75
  Requires-Dist: uvicorn[standard]>=0.23.2; extra == 'docs'
76
76
  Description-Content-Type: text/markdown
77
77
 
@@ -301,7 +301,7 @@ class NcSessionBasic(NcSessionBase, ABC):
301
301
 
302
302
  def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
303
303
  adapter = self.adapter_dav if dav else self.adapter
304
- with adapter.stream("GET", url_path, params=params) as response:
304
+ with adapter.stream("GET", url_path, params=params, headers=kwargs.get("headers")) as response:
305
305
  check_error(response)
306
306
  for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
307
307
  fp.write(data_chunk)
@@ -425,7 +425,7 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
425
425
 
426
426
  async def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
427
427
  adapter = self.adapter_dav if dav else self.adapter
428
- async with adapter.stream("GET", url_path, params=params) as response:
428
+ async with adapter.stream("GET", url_path, params=params, headers=kwargs.get("headers")) as response:
429
429
  check_error(response)
430
430
  async for data_chunk in response.aiter_raw(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
431
431
  fp.write(data_chunk)
@@ -1,3 +1,3 @@
1
1
  """Version of nc_py_api."""
2
2
 
3
- __version__ = "0.17.0"
3
+ __version__ = "0.18.0"
@@ -1,7 +1,6 @@
1
1
  """Nextcloud API for working with applications."""
2
2
 
3
3
  import dataclasses
4
- import datetime
5
4
 
6
5
  from ._misc import require_capabilities
7
6
  from ._session import AsyncNcSessionBasic, NcSessionBasic
@@ -34,18 +33,6 @@ class ExAppInfo:
34
33
  """Flag indicating if the application enabled."""
35
34
  return bool(self._raw_data["enabled"])
36
35
 
37
- @property
38
- def last_check_time(self) -> datetime.datetime:
39
- """Time of the last successful application check."""
40
- return datetime.datetime.utcfromtimestamp(int(self._raw_data["last_check_time"])).replace(
41
- tzinfo=datetime.timezone.utc
42
- )
43
-
44
- @property
45
- def system(self) -> bool:
46
- """**DEPRECATED** Flag indicating if the application is a system application."""
47
- return True
48
-
49
36
  def __repr__(self):
50
37
  return f"<{self.__class__.__name__} id={self.app_id}, ver={self.version}>"
51
38
 
@@ -10,6 +10,7 @@ from .integration_fastapi import (
10
10
  set_handlers,
11
11
  talk_bot_msg,
12
12
  )
13
+ from .logging import setup_nextcloud_logging
13
14
  from .misc import (
14
15
  get_computation_device,
15
16
  get_model_path,
@@ -0,0 +1,46 @@
1
+ """Transparent logging support to store logs in the nextcloud.log."""
2
+
3
+ import logging
4
+ import threading
5
+
6
+ from ..nextcloud import NextcloudApp
7
+ from .defs import LogLvl
8
+
9
+ LOGLVL_MAP = {
10
+ logging.NOTSET: LogLvl.DEBUG,
11
+ logging.DEBUG: LogLvl.DEBUG,
12
+ logging.INFO: LogLvl.INFO,
13
+ logging.WARNING: LogLvl.WARNING,
14
+ logging.ERROR: LogLvl.ERROR,
15
+ logging.CRITICAL: LogLvl.FATAL,
16
+ }
17
+
18
+ THREAD_LOCAL = threading.local()
19
+
20
+
21
+ class _NextcloudLogsHandler(logging.Handler):
22
+ def __init__(self):
23
+ super().__init__()
24
+
25
+ def emit(self, record):
26
+ if THREAD_LOCAL.__dict__.get("nc_py_api.loghandler", False):
27
+ return
28
+
29
+ try:
30
+ THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = True
31
+ log_entry = self.format(record)
32
+ log_level = record.levelno
33
+ NextcloudApp().log(LOGLVL_MAP.get(log_level, LogLvl.FATAL), log_entry, fast_send=True)
34
+ except Exception: # noqa pylint: disable=broad-exception-caught
35
+ self.handleError(record)
36
+ finally:
37
+ THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = False
38
+
39
+
40
+ def setup_nextcloud_logging(logger_name: str | None = None, logging_level: int = logging.DEBUG):
41
+ """Function to easily send all or selected log entries to Nextcloud."""
42
+ logger = logging.getLogger(logger_name)
43
+ nextcloud_handler = _NextcloudLogsHandler()
44
+ nextcloud_handler.setLevel(logging_level)
45
+ logger.addHandler(nextcloud_handler)
46
+ return nextcloud_handler
@@ -5,12 +5,18 @@ import datetime
5
5
  import email.utils
6
6
  import enum
7
7
  import os
8
+ import re
8
9
  import warnings
9
10
 
10
11
  from pydantic import BaseModel
11
12
 
12
13
  from .. import _misc
13
14
 
15
+ user_regex = re.compile(r"(?:files|trashbin|versions)/([^/]+)/")
16
+ """Regex for evaluating user from full path string; instantiated once on import."""
17
+ user_path_regex = re.compile(r".*?(files|trashbin|versions)/([^/]+)/")
18
+ """Regex for evaluating user path from full path string; instantiated once on import."""
19
+
14
20
 
15
21
  class LockType(enum.IntEnum):
16
22
  """Nextcloud File Locks types."""
@@ -218,12 +224,12 @@ class FsNode:
218
224
  @property
219
225
  def user(self) -> str:
220
226
  """Returns user ID extracted from the `full_path`."""
221
- return self.full_path.lstrip("/").split("/", maxsplit=2)[1]
227
+ return user_regex.findall(self.full_path)[0]
222
228
 
223
229
  @property
224
230
  def user_path(self) -> str:
225
231
  """Returns path relative to the user's root directory."""
226
- return self.full_path.lstrip("/").split("/", maxsplit=2)[-1]
232
+ return user_path_regex.sub("", self.full_path, count=1)
227
233
 
228
234
  @property
229
235
  def is_shared(self) -> bool:
@@ -115,9 +115,14 @@ class FilesAPI:
115
115
  path = path.user_path if isinstance(path, FsNode) else path
116
116
  result_path = local_path if local_path else os.path.basename(path)
117
117
  with open(result_path, "wb") as fp:
118
- self._session.download2fp(
119
- "/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
120
- )
118
+ if self._session.nc_version["major"] >= 31:
119
+ full_path = dav_get_obj_path(self._session.user, path)
120
+ accept_header = f"application/{kwargs.get('format', 'zip')}"
121
+ self._session.download2fp(quote(full_path), fp, dav=True, headers={"Accept": accept_header})
122
+ else:
123
+ self._session.download2fp(
124
+ "/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
125
+ )
121
126
  return Path(result_path)
122
127
 
123
128
  def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
@@ -119,9 +119,14 @@ class AsyncFilesAPI:
119
119
  path = path.user_path if isinstance(path, FsNode) else path
120
120
  result_path = local_path if local_path else os.path.basename(path)
121
121
  with open(result_path, "wb") as fp:
122
- await self._session.download2fp(
123
- "/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
124
- )
122
+ if (await self._session.nc_version)["major"] >= 31:
123
+ full_path = dav_get_obj_path(await self._session.user, path)
124
+ accept_header = f"application/{kwargs.get('format', 'zip')}"
125
+ await self._session.download2fp(quote(full_path), fp, dav=True, headers={"Accept": accept_header})
126
+ else:
127
+ await self._session.download2fp(
128
+ "/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
129
+ )
125
130
  return Path(result_path)
126
131
 
127
132
  async def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
@@ -348,13 +348,18 @@ class NextcloudApp(_NextcloudBasic):
348
348
  return bool(self._session.ocs("GET", "/ocs/v1.php/apps/app_api/ex-app/state"))
349
349
  return False
350
350
 
351
- def log(self, log_lvl: LogLvl, content: str) -> None:
351
+ def log(self, log_lvl: LogLvl, content: str, fast_send: bool = False) -> None:
352
352
  """Writes log to the Nextcloud log file."""
353
- if self.check_capabilities("app_api"):
354
- return
355
- if int(log_lvl) < self.capabilities["app_api"].get("loglevel", 0):
356
- return
357
- self._session.ocs("POST", f"{self._session.ae_url}/log", json={"level": int(log_lvl), "message": content})
353
+ int_log_lvl = int(log_lvl)
354
+ if int_log_lvl < 0 or int_log_lvl > 4:
355
+ raise ValueError("Invalid `log_lvl` value")
356
+ if not fast_send:
357
+ if self.check_capabilities("app_api"):
358
+ return
359
+ if int_log_lvl < self.capabilities["app_api"].get("loglevel", 0):
360
+ return
361
+ with contextlib.suppress(Exception):
362
+ self._session.ocs("POST", f"{self._session.ae_url}/log", json={"level": int_log_lvl, "message": content})
358
363
 
359
364
  def users_list(self) -> list[str]:
360
365
  """Returns list of users on the Nextcloud instance."""
@@ -478,13 +483,20 @@ class AsyncNextcloudApp(_AsyncNextcloudBasic):
478
483
  return bool(await self._session.ocs("GET", "/ocs/v1.php/apps/app_api/ex-app/state"))
479
484
  return False
480
485
 
481
- async def log(self, log_lvl: LogLvl, content: str) -> None:
486
+ async def log(self, log_lvl: LogLvl, content: str, fast_send: bool = False) -> None:
482
487
  """Writes log to the Nextcloud log file."""
483
- if await self.check_capabilities("app_api"):
484
- return
485
- if int(log_lvl) < (await self.capabilities)["app_api"].get("loglevel", 0):
486
- return
487
- await self._session.ocs("POST", f"{self._session.ae_url}/log", json={"level": int(log_lvl), "message": content})
488
+ int_log_lvl = int(log_lvl)
489
+ if int_log_lvl < 0 or int_log_lvl > 4:
490
+ raise ValueError("Invalid `log_lvl` value")
491
+ if not fast_send:
492
+ if await self.check_capabilities("app_api"):
493
+ return
494
+ if int_log_lvl < (await self.capabilities)["app_api"].get("loglevel", 0):
495
+ return
496
+ with contextlib.suppress(Exception):
497
+ await self._session.ocs(
498
+ "POST", f"{self._session.ae_url}/log", json={"level": int_log_lvl, "message": content}
499
+ )
488
500
 
489
501
  async def users_list(self) -> list[str]:
490
502
  """Returns list of users on the Nextcloud instance."""
@@ -369,8 +369,8 @@ class _AsyncUsersAPI:
369
369
 
370
370
 
371
371
  def _create(user_id: str, display_name: str | None, **kwargs) -> dict[str, typing.Any]:
372
- password = kwargs.get("password", None)
373
- email = kwargs.get("email", None)
372
+ password = kwargs.get("password")
373
+ email = kwargs.get("email")
374
374
  if not password and not email:
375
375
  raise ValueError("Either password or email must be set")
376
376
  data = {"userid": user_id}
@@ -3,7 +3,7 @@
3
3
  import dataclasses
4
4
 
5
5
  from ._misc import clear_from_params_empty # , require_capabilities
6
- from ._session import AsyncNcSessionBasic, NcSessionBasic
6
+ from ._session import AppConfig, AsyncNcSessionBasic, NcSessionBasic
7
7
 
8
8
 
9
9
  @dataclasses.dataclass
@@ -140,6 +140,13 @@ class _WebhooksAPI:
140
140
  def unregister(self, webhook_id: int) -> bool:
141
141
  return self._session.ocs("DELETE", f"{self._ep_base}/{webhook_id}")
142
142
 
143
+ def unregister_all(self, appid: str = "") -> int:
144
+ if not appid and isinstance(self._session.cfg, AppConfig):
145
+ appid = self._session.cfg.app_name
146
+ else:
147
+ raise ValueError("The `appid` parameter cannot be empty for non-ExApp use.")
148
+ return self._session.ocs("DELETE", f"{self._ep_base}/byappid/{appid}")
149
+
143
150
 
144
151
  class _AsyncWebhooksAPI:
145
152
  """The class provides the async application management API on the Nextcloud server."""
@@ -208,3 +215,10 @@ class _AsyncWebhooksAPI:
208
215
 
209
216
  async def unregister(self, webhook_id: int) -> bool:
210
217
  return await self._session.ocs("DELETE", f"{self._ep_base}/{webhook_id}")
218
+
219
+ async def unregister_all(self, appid: str = "") -> int:
220
+ if not appid and isinstance(self._session.cfg, AppConfig):
221
+ appid = self._session.cfg.app_name
222
+ else:
223
+ raise ValueError("The `appid` parameter cannot be empty for non-ExApp use.")
224
+ return await self._session.ocs("DELETE", f"{self._ep_base}/byappid/{appid}")
@@ -77,11 +77,11 @@ optional-dependencies.dev-min = [
77
77
  optional-dependencies.docs = [
78
78
  "autodoc-pydantic>=2.0.1",
79
79
  "nc-py-api[app,calendar]",
80
- "sphinx>=6.2",
80
+ "sphinx<8",
81
81
  "sphinx-copybutton",
82
82
  "sphinx-inline-tabs",
83
83
  "sphinx-issues>=3.0.1",
84
- "sphinx-rtd-theme>=1",
84
+ "sphinx-rtd-theme<3",
85
85
  ]
86
86
  urls.Changelog = "https://github.com/cloud-py-api/nc_py_api/blob/main/CHANGELOG.md"
87
87
  urls.Documentation = "https://cloud-py-api.github.io/nc_py_api/"
@@ -207,6 +207,7 @@ messages_control.disable = [
207
207
  "too-few-public-methods",
208
208
  "too-many-public-methods",
209
209
  "too-many-instance-attributes",
210
+ "too-many-positional-arguments",
210
211
  ]
211
212
 
212
213
  [tool.pytest.ini_options]
File without changes
File without changes
File without changes
File without changes