nc-py-api 0.20.2__py3-none-any.whl → 0.21.1__py3-none-any.whl

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.
nc_py_api/__init__.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from . import ex_app, options
4
4
  from ._exceptions import (
5
+ ModelFetchError,
5
6
  NextcloudException,
6
7
  NextcloudExceptionNotFound,
7
8
  NextcloudMissingCapabilities,
nc_py_api/_exceptions.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Exceptions for the Nextcloud API."""
2
2
 
3
- from httpx import Response, codes
3
+ from niquests import HTTPError, Response
4
4
 
5
5
 
6
6
  class NextcloudException(Exception):
@@ -60,6 +60,12 @@ def check_error(response: Response, info: str = ""):
60
60
  else:
61
61
  phrase = "Unknown error"
62
62
  raise NextcloudException(status_code, reason=phrase, info=info)
63
- if not codes.is_error(status_code):
64
- return
65
- raise NextcloudException(status_code, reason=codes(status_code).phrase, info=info)
63
+
64
+ try:
65
+ response.raise_for_status()
66
+ except HTTPError as e:
67
+ raise NextcloudException(status_code, reason=response.reason, info=info) from e
68
+
69
+
70
+ class ModelFetchError(Exception):
71
+ """Exception raised when model fetching fails."""
nc_py_api/_session.py CHANGED
@@ -10,9 +10,11 @@ from dataclasses import dataclass
10
10
  from enum import IntEnum
11
11
  from json import loads
12
12
  from os import environ
13
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
13
14
 
14
- from httpx import AsyncClient, Client, Headers, Limits, ReadTimeout, Request, Response
15
- from httpx import __version__ as httpx_version
15
+ from niquests import AsyncSession, ReadTimeout, Request, Response, Session
16
+ from niquests import __version__ as niquests_version
17
+ from niquests.structures import CaseInsensitiveDict
16
18
  from starlette.requests import HTTPConnection
17
19
 
18
20
  from . import options
@@ -49,6 +51,13 @@ class ServerVersion(typing.TypedDict):
49
51
  """Indicates if the subscription has extended support"""
50
52
 
51
53
 
54
+ @dataclass
55
+ class Limits:
56
+ max_keepalive_connections: int | None = 20
57
+ max_connections: int | None = 100
58
+ keepalive_expiry: int | float | None = 5
59
+
60
+
52
61
  @dataclass
53
62
  class RuntimeOptions:
54
63
  xdebug_session: str
@@ -134,11 +143,11 @@ class AppConfig(BasicConfig):
134
143
 
135
144
 
136
145
  class NcSessionBase(ABC):
137
- adapter: AsyncClient | Client
138
- adapter_dav: AsyncClient | Client
146
+ adapter: AsyncSession | Session
147
+ adapter_dav: AsyncSession | Session
139
148
  cfg: BasicConfig
140
149
  custom_headers: dict
141
- response_headers: Headers
150
+ response_headers: CaseInsensitiveDict
142
151
  _user: str
143
152
  _capabilities: dict
144
153
 
@@ -150,7 +159,7 @@ class NcSessionBase(ABC):
150
159
  self.limits = Limits(max_keepalive_connections=20, max_connections=20, keepalive_expiry=60.0)
151
160
  self.init_adapter()
152
161
  self.init_adapter_dav()
153
- self.response_headers = Headers()
162
+ self.response_headers = CaseInsensitiveDict()
154
163
  self._ocs_regexp = re.compile(r"/ocs/v[12]\.php/|/apps/groupfolders/")
155
164
 
156
165
  def init_adapter(self, restart=False) -> None:
@@ -172,7 +181,7 @@ class NcSessionBase(ABC):
172
181
  self.adapter_dav.cookies.set("XDEBUG_SESSION", options.XDEBUG_SESSION)
173
182
 
174
183
  @abstractmethod
175
- def _create_adapter(self, dav: bool = False) -> AsyncClient | Client:
184
+ def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
176
185
  pass # pragma: no cover
177
186
 
178
187
  @property
@@ -187,8 +196,8 @@ class NcSessionBase(ABC):
187
196
 
188
197
 
189
198
  class NcSessionBasic(NcSessionBase, ABC):
190
- adapter: Client
191
- adapter_dav: Client
199
+ adapter: Session
200
+ adapter_dav: Session
192
201
 
193
202
  def ocs(
194
203
  self,
@@ -206,9 +215,7 @@ class NcSessionBasic(NcSessionBase, ABC):
206
215
  info = f"request: {method} {path}"
207
216
  nested_req = kwargs.pop("nested_req", False)
208
217
  try:
209
- response = self.adapter.request(
210
- method, path, content=content, json=json, params=params, files=files, **kwargs
211
- )
218
+ response = self.adapter.request(method, path, data=content, json=json, params=params, files=files, **kwargs)
212
219
  except ReadTimeout:
213
220
  raise NextcloudException(408, info=info) from None
214
221
 
@@ -281,18 +288,18 @@ class NcSessionBasic(NcSessionBase, ABC):
281
288
  return {
282
289
  "base_url": self.cfg.dav_endpoint,
283
290
  "timeout": self.cfg.options.timeout_dav,
284
- "event_hooks": {"request": [], "response": [self._response_event]},
291
+ "event_hooks": {"pre_request": [], "response": [self._response_event]},
285
292
  }
286
293
  return {
287
294
  "base_url": self.cfg.endpoint,
288
295
  "timeout": self.cfg.options.timeout,
289
- "event_hooks": {"request": [self._request_event_ocs], "response": [self._response_event]},
296
+ "event_hooks": {"pre_request": [self._request_event_ocs], "response": [self._response_event]},
290
297
  }
291
298
 
292
299
  def _request_event_ocs(self, request: Request) -> None:
293
300
  str_url = str(request.url)
294
301
  if re.search(self._ocs_regexp, str_url) is not None: # this is OCS call
295
- request.url = request.url.copy_merge_params({"format": "json"})
302
+ request.url = patch_param(request.url, "format", "json")
296
303
  request.headers["Accept"] = "application/json"
297
304
 
298
305
  def _response_event(self, response: Response) -> None:
@@ -305,15 +312,15 @@ class NcSessionBasic(NcSessionBase, ABC):
305
312
 
306
313
  def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
307
314
  adapter = self.adapter_dav if dav else self.adapter
308
- with adapter.stream("GET", url_path, params=params, headers=kwargs.get("headers")) as response:
315
+ with adapter.get(url_path, params=params, headers=kwargs.get("headers"), stream=True) as response:
309
316
  check_error(response)
310
- for data_chunk in response.iter_bytes(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
317
+ for data_chunk in response.iter_raw(chunk_size=kwargs.get("chunk_size", -1)):
311
318
  fp.write(data_chunk)
312
319
 
313
320
 
314
321
  class AsyncNcSessionBasic(NcSessionBase, ABC):
315
- adapter: AsyncClient
316
- adapter_dav: AsyncClient
322
+ adapter: AsyncSession
323
+ adapter_dav: AsyncSession
317
324
 
318
325
  async def ocs(
319
326
  self,
@@ -332,7 +339,7 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
332
339
  nested_req = kwargs.pop("nested_req", False)
333
340
  try:
334
341
  response = await self.adapter.request(
335
- method, path, content=content, json=json, params=params, files=files, **kwargs
342
+ method, path, data=content, json=json, params=params, files=files, **kwargs
336
343
  )
337
344
  except ReadTimeout:
338
345
  raise NextcloudException(408, info=info) from None
@@ -350,7 +357,7 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
350
357
  and ocs_meta["statuscode"] == 403
351
358
  and str(ocs_meta["message"]).lower().find("password confirmation is required") != -1
352
359
  ):
353
- await self.adapter.aclose()
360
+ await self.adapter.close()
354
361
  self.init_adapter(restart=True)
355
362
  return await self.ocs(
356
363
  method, path, **kwargs, content=content, json=json, params=params, nested_req=True
@@ -408,18 +415,18 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
408
415
  return {
409
416
  "base_url": self.cfg.dav_endpoint,
410
417
  "timeout": self.cfg.options.timeout_dav,
411
- "event_hooks": {"request": [], "response": [self._response_event]},
418
+ "event_hooks": {"pre_request": [], "response": [self._response_event]},
412
419
  }
413
420
  return {
414
421
  "base_url": self.cfg.endpoint,
415
422
  "timeout": self.cfg.options.timeout,
416
- "event_hooks": {"request": [self._request_event_ocs], "response": [self._response_event]},
423
+ "event_hooks": {"pre_request": [self._request_event_ocs], "response": [self._response_event]},
417
424
  }
418
425
 
419
426
  async def _request_event_ocs(self, request: Request) -> None:
420
427
  str_url = str(request.url)
421
428
  if re.search(self._ocs_regexp, str_url) is not None: # this is OCS call
422
- request.url = request.url.copy_merge_params({"format": "json"})
429
+ request.url = patch_param(request.url, "format", "json")
423
430
  request.headers["Accept"] = "application/json"
424
431
 
425
432
  async def _response_event(self, response: Response) -> None:
@@ -432,10 +439,12 @@ class AsyncNcSessionBasic(NcSessionBase, ABC):
432
439
 
433
440
  async def download2fp(self, url_path: str, fp, dav: bool, params=None, **kwargs):
434
441
  adapter = self.adapter_dav if dav else self.adapter
435
- async with adapter.stream("GET", url_path, params=params, headers=kwargs.get("headers")) as response:
436
- check_error(response)
437
- async for data_chunk in response.aiter_bytes(chunk_size=kwargs.get("chunk_size", 5 * 1024 * 1024)):
438
- fp.write(data_chunk)
442
+ response = await adapter.get(url_path, params=params, headers=kwargs.get("headers"), stream=True)
443
+
444
+ check_error(response)
445
+
446
+ async for data_chunk in await response.iter_raw(chunk_size=kwargs.get("chunk_size", -1)):
447
+ fp.write(data_chunk)
439
448
 
440
449
 
441
450
  class NcSession(NcSessionBasic):
@@ -445,15 +454,20 @@ class NcSession(NcSessionBasic):
445
454
  self.cfg = Config(**kwargs)
446
455
  super().__init__()
447
456
 
448
- def _create_adapter(self, dav: bool = False) -> AsyncClient | Client:
449
- return Client(
450
- follow_redirects=True,
451
- limits=self.limits,
452
- verify=self.cfg.options.nc_cert,
453
- **self._get_adapter_kwargs(dav),
454
- auth=self.cfg.auth,
457
+ def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
458
+ session_kwargs = self._get_adapter_kwargs(dav)
459
+ hooks = session_kwargs.pop("event_hooks")
460
+
461
+ session = Session(
462
+ keepalive_delay=self.limits.keepalive_expiry, pool_maxsize=self.limits.max_connections, **session_kwargs
455
463
  )
456
464
 
465
+ session.auth = self.cfg.auth
466
+ session.verify = self.cfg.options.nc_cert
467
+ session.hooks.update(hooks)
468
+
469
+ return session
470
+
457
471
 
458
472
  class AsyncNcSession(AsyncNcSessionBasic):
459
473
  cfg: Config
@@ -462,21 +476,28 @@ class AsyncNcSession(AsyncNcSessionBasic):
462
476
  self.cfg = Config(**kwargs)
463
477
  super().__init__()
464
478
 
465
- def _create_adapter(self, dav: bool = False) -> AsyncClient | Client:
466
- return AsyncClient(
467
- follow_redirects=True,
468
- limits=self.limits,
469
- verify=self.cfg.options.nc_cert,
470
- **self._get_adapter_kwargs(dav),
471
- auth=self.cfg.auth,
479
+ def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
480
+ session_kwargs = self._get_adapter_kwargs(dav)
481
+ hooks = session_kwargs.pop("event_hooks")
482
+
483
+ session = AsyncSession(
484
+ keepalive_delay=self.limits.keepalive_expiry,
485
+ pool_maxsize=self.limits.max_connections,
486
+ **session_kwargs,
472
487
  )
473
488
 
489
+ session.verify = self.cfg.options.nc_cert
490
+ session.auth = self.cfg.auth
491
+ session.hooks.update(hooks)
492
+
493
+ return session
494
+
474
495
 
475
496
  class NcSessionAppBasic(ABC):
476
497
  cfg: AppConfig
477
498
  _user: str
478
- adapter: AsyncClient | Client
479
- adapter_dav: AsyncClient | Client
499
+ adapter: AsyncSession | Session
500
+ adapter_dav: AsyncSession | Session
480
501
 
481
502
  def __init__(self, **kwargs):
482
503
  self.cfg = AppConfig(**kwargs)
@@ -505,22 +526,29 @@ class NcSessionAppBasic(ABC):
505
526
  class NcSessionApp(NcSessionAppBasic, NcSessionBasic):
506
527
  cfg: AppConfig
507
528
 
508
- def _create_adapter(self, dav: bool = False) -> AsyncClient | Client:
509
- r = self._get_adapter_kwargs(dav)
510
- r["event_hooks"]["request"].append(self._add_auth)
511
- return Client(
512
- follow_redirects=True,
513
- limits=self.limits,
514
- verify=self.cfg.options.nc_cert,
515
- **r,
516
- headers={
517
- "AA-VERSION": self.cfg.aa_version,
518
- "EX-APP-ID": self.cfg.app_name,
519
- "EX-APP-VERSION": self.cfg.app_version,
520
- "user-agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (httpx/{httpx_version})",
521
- },
529
+ def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
530
+ session_kwargs = self._get_adapter_kwargs(dav)
531
+ session_kwargs["event_hooks"]["pre_request"].append(self._add_auth)
532
+
533
+ hooks = session_kwargs.pop("event_hooks")
534
+
535
+ session = Session(
536
+ keepalive_delay=self.limits.keepalive_expiry,
537
+ pool_maxsize=self.limits.max_connections,
538
+ **session_kwargs,
522
539
  )
523
540
 
541
+ session.verify = self.cfg.options.nc_cert
542
+ session.headers = {
543
+ "AA-VERSION": self.cfg.aa_version,
544
+ "EX-APP-ID": self.cfg.app_name,
545
+ "EX-APP-VERSION": self.cfg.app_version,
546
+ "user-agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (niquests/{niquests_version})",
547
+ }
548
+ session.hooks.update(hooks)
549
+
550
+ return session
551
+
524
552
  def _add_auth(self, request: Request):
525
553
  request.headers.update(
526
554
  {"AUTHORIZATION-APP-API": b64encode(f"{self._user}:{self.cfg.app_secret}".encode("UTF=8"))}
@@ -530,23 +558,39 @@ class NcSessionApp(NcSessionAppBasic, NcSessionBasic):
530
558
  class AsyncNcSessionApp(NcSessionAppBasic, AsyncNcSessionBasic):
531
559
  cfg: AppConfig
532
560
 
533
- def _create_adapter(self, dav: bool = False) -> AsyncClient | Client:
534
- r = self._get_adapter_kwargs(dav)
535
- r["event_hooks"]["request"].append(self._add_auth)
536
- return AsyncClient(
537
- follow_redirects=True,
538
- limits=self.limits,
539
- verify=self.cfg.options.nc_cert,
540
- **r,
541
- headers={
542
- "AA-VERSION": self.cfg.aa_version,
543
- "EX-APP-ID": self.cfg.app_name,
544
- "EX-APP-VERSION": self.cfg.app_version,
545
- "User-Agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (httpx/{httpx_version})",
546
- },
561
+ def _create_adapter(self, dav: bool = False) -> AsyncSession | Session:
562
+ session_kwargs = self._get_adapter_kwargs(dav)
563
+ session_kwargs["event_hooks"]["pre_request"].append(self._add_auth)
564
+
565
+ hooks = session_kwargs.pop("event_hooks")
566
+
567
+ session = AsyncSession(
568
+ keepalive_delay=self.limits.keepalive_expiry,
569
+ pool_maxsize=self.limits.max_connections,
570
+ **session_kwargs,
547
571
  )
572
+ session.verify = self.cfg.options.nc_cert
573
+ session.headers = {
574
+ "AA-VERSION": self.cfg.aa_version,
575
+ "EX-APP-ID": self.cfg.app_name,
576
+ "EX-APP-VERSION": self.cfg.app_version,
577
+ "User-Agent": f"ExApp/{self.cfg.app_name}/{self.cfg.app_version} (niquests/{niquests_version})",
578
+ }
579
+ session.hooks.update(hooks)
580
+
581
+ return session
548
582
 
549
583
  async def _add_auth(self, request: Request):
550
584
  request.headers.update(
551
585
  {"AUTHORIZATION-APP-API": b64encode(f"{self._user}:{self.cfg.app_secret}".encode("UTF=8"))}
552
586
  )
587
+
588
+
589
+ def patch_param(url: str, key: str, value: str) -> str:
590
+ parts = urlsplit(url)
591
+ query = dict(parse_qsl(parts.query, keep_blank_values=True))
592
+ query[key] = value
593
+
594
+ new_query = urlencode(query, doseq=True)
595
+
596
+ return urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment))
nc_py_api/_version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version of nc_py_api."""
2
2
 
3
- __version__ = "0.20.2"
3
+ __version__ = "0.21.1"
nc_py_api/calendar_api.py CHANGED
@@ -23,7 +23,7 @@ try:
23
23
  if body:
24
24
  body = body.replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n")
25
25
  r = self._session.adapter_dav.request(
26
- method, url if isinstance(url, str) else str(url), content=body, headers=headers
26
+ method, url if isinstance(url, str) else str(url), data=body, headers=headers
27
27
  )
28
28
  return DAVResponse(r)
29
29
 
@@ -7,9 +7,10 @@ import hashlib
7
7
  import json
8
8
  import os
9
9
  import typing
10
+ from traceback import format_exc
10
11
  from urllib.parse import urlparse
11
12
 
12
- import httpx
13
+ import niquests
13
14
  from fastapi import (
14
15
  BackgroundTasks,
15
16
  Depends,
@@ -22,10 +23,10 @@ from fastapi.responses import JSONResponse, PlainTextResponse
22
23
  from starlette.requests import HTTPConnection, Request
23
24
  from starlette.types import ASGIApp, Receive, Scope, Send
24
25
 
26
+ from .._exceptions import ModelFetchError
25
27
  from .._misc import get_username_secret_from_headers
26
28
  from ..nextcloud import AsyncNextcloudApp, NextcloudApp
27
29
  from ..talk_bot import TalkBotMessage
28
- from .defs import LogLvl
29
30
  from .misc import persistent_storage
30
31
 
31
32
 
@@ -70,9 +71,24 @@ def set_handlers(
70
71
 
71
72
  .. note:: When this parameter is ``False``, the provision of ``models_to_fetch`` is not allowed.
72
73
 
73
- :param models_to_fetch: Dictionary describing which models should be downloaded during `init`.
74
+ :param models_to_fetch: Dictionary describing which models should be downloaded during `init` of the form:
75
+ .. code-block:: python
76
+ {
77
+ "model_url_1": {
78
+ "save_path": "path_or_filename_to_save_the_model_to",
79
+ },
80
+ "huggingface_model_name_1": {
81
+ "max_workers": 4,
82
+ "cache_dir": "path_to_cache_dir",
83
+ "revision": "revision_to_fetch",
84
+ ...
85
+ },
86
+ ...
87
+ }
88
+
74
89
 
75
90
  .. note:: ``huggingface_hub`` package should be present for automatic models fetching.
91
+ All model options are optional and can be left empty.
76
92
 
77
93
  :param map_app_static: Should be folders ``js``, ``css``, ``l10n``, ``img`` automatically mounted in FastAPI or not.
78
94
 
@@ -121,73 +137,98 @@ def __map_app_static_folders(fast_api_app: FastAPI):
121
137
 
122
138
 
123
139
  def fetch_models_task(nc: NextcloudApp, models: dict[str, dict], progress_init_start_value: int) -> None:
124
- """Use for cases when you want to define custom `/init` but still need to easy download models."""
140
+ """Use for cases when you want to define custom `/init` but still need to easy download models.
141
+
142
+ :param nc: NextcloudApp instance.
143
+ :param models_to_fetch: Dictionary describing which models should be downloaded of the form:
144
+ .. code-block:: python
145
+ {
146
+ "model_url_1": {
147
+ "save_path": "path_or_filename_to_save_the_model_to",
148
+ },
149
+ "huggingface_model_name_1": {
150
+ "max_workers": 4,
151
+ "cache_dir": "path_to_cache_dir",
152
+ "revision": "revision_to_fetch",
153
+ ...
154
+ },
155
+ ...
156
+ }
157
+
158
+ .. note:: ``huggingface_hub`` package should be present for automatic models fetching.
159
+ All model options are optional and can be left empty.
160
+
161
+ :param progress_init_start_value: Integer value defining from which percent the progress should start.
162
+
163
+ :raises ModelFetchError: in case of a model download error.
164
+ :raises NextcloudException: in case of a network error reaching the Nextcloud server.
165
+ """
125
166
  if models:
126
167
  current_progress = progress_init_start_value
127
168
  percent_for_each = min(int((100 - progress_init_start_value) / len(models)), 99)
128
169
  for model in models:
129
- if model.startswith(("http://", "https://")):
130
- models[model]["path"] = __fetch_model_as_file(
131
- current_progress, percent_for_each, nc, model, models[model]
132
- )
133
- else:
134
- models[model]["path"] = __fetch_model_as_snapshot(
135
- current_progress, percent_for_each, nc, model, models[model]
136
- )
137
- current_progress += percent_for_each
170
+ try:
171
+ if model.startswith(("http://", "https://")):
172
+ models[model]["path"] = __fetch_model_as_file(
173
+ current_progress, percent_for_each, nc, model, models[model]
174
+ )
175
+ else:
176
+ models[model]["path"] = __fetch_model_as_snapshot(
177
+ current_progress, percent_for_each, nc, model, models[model]
178
+ )
179
+ current_progress += percent_for_each
180
+ except BaseException as e: # noqa pylint: disable=broad-exception-caught
181
+ nc.set_init_status(current_progress, f"Downloading of '{model}' failed: {e}: {format_exc()}")
182
+ raise ModelFetchError(f"Downloading of '{model}' failed.") from e
138
183
  nc.set_init_status(100)
139
184
 
140
185
 
141
186
  def __fetch_model_as_file(
142
187
  current_progress: int, progress_for_task: int, nc: NextcloudApp, model_path: str, download_options: dict
143
- ) -> str | None:
188
+ ) -> str:
144
189
  result_path = download_options.pop("save_path", urlparse(model_path).path.split("/")[-1])
145
- try:
146
- with httpx.stream("GET", model_path, follow_redirects=True) as response:
147
- if not response.is_success:
148
- nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' returned {response.status_code} status.")
149
- return None
150
- downloaded_size = 0
151
- linked_etag = ""
152
- for each_history in response.history:
153
- linked_etag = each_history.headers.get("X-Linked-ETag", "")
154
- if linked_etag:
155
- break
156
- if not linked_etag:
157
- linked_etag = response.headers.get("X-Linked-ETag", response.headers.get("ETag", ""))
158
- total_size = int(response.headers.get("Content-Length"))
159
- try:
160
- existing_size = os.path.getsize(result_path)
161
- except OSError:
162
- existing_size = 0
163
- if linked_etag and total_size == existing_size:
164
- with builtins.open(result_path, "rb") as file:
165
- sha256_hash = hashlib.sha256()
166
- for byte_block in iter(lambda: file.read(4096), b""):
167
- sha256_hash.update(byte_block)
168
- if f'"{sha256_hash.hexdigest()}"' == linked_etag:
169
- nc.set_init_status(min(current_progress + progress_for_task, 99))
170
- return None
171
-
172
- with builtins.open(result_path, "wb") as file:
173
- last_progress = current_progress
174
- for chunk in response.iter_bytes(5 * 1024 * 1024):
175
- downloaded_size += file.write(chunk)
176
- if total_size:
177
- new_progress = min(current_progress + int(progress_for_task * downloaded_size / total_size), 99)
178
- if new_progress != last_progress:
179
- nc.set_init_status(new_progress)
180
- last_progress = new_progress
181
-
182
- return result_path
183
- except Exception as e: # noqa pylint: disable=broad-exception-caught
184
- nc.log(LogLvl.ERROR, f"Downloading of '{model_path}' raised an exception: {e}")
185
-
186
- return None
190
+ with niquests.get(model_path, stream=True) as response:
191
+ if not response.ok:
192
+ raise ModelFetchError(
193
+ f"Downloading of '{model_path}' failed, returned ({response.status_code}) {response.text}"
194
+ )
195
+ downloaded_size = 0
196
+ linked_etag = ""
197
+ for each_history in response.history:
198
+ linked_etag = each_history.headers.get("X-Linked-ETag", "")
199
+ if linked_etag:
200
+ break
201
+ if not linked_etag:
202
+ linked_etag = response.headers.get("X-Linked-ETag", response.headers.get("ETag", ""))
203
+ total_size = int(response.headers.get("Content-Length"))
204
+ try:
205
+ existing_size = os.path.getsize(result_path)
206
+ except OSError:
207
+ existing_size = 0
208
+ if linked_etag and total_size == existing_size:
209
+ with builtins.open(result_path, "rb") as file:
210
+ sha256_hash = hashlib.sha256()
211
+ for byte_block in iter(lambda: file.read(4096), b""):
212
+ sha256_hash.update(byte_block)
213
+ if f'"{sha256_hash.hexdigest()}"' == linked_etag:
214
+ nc.set_init_status(min(current_progress + progress_for_task, 99))
215
+ return result_path
216
+
217
+ with builtins.open(result_path, "wb") as file:
218
+ last_progress = current_progress
219
+ for chunk in response.iter_raw(-1):
220
+ downloaded_size += file.write(chunk)
221
+ if total_size:
222
+ new_progress = min(current_progress + int(progress_for_task * downloaded_size / total_size), 99)
223
+ if new_progress != last_progress:
224
+ nc.set_init_status(new_progress)
225
+ last_progress = new_progress
226
+
227
+ return result_path
187
228
 
188
229
 
189
230
  def __fetch_model_as_snapshot(
190
- current_progress: int, progress_for_task, nc: NextcloudApp, mode_name: str, download_options: dict
231
+ current_progress: int, progress_for_task, nc: NextcloudApp, model_name: str, download_options: dict
191
232
  ) -> str:
192
233
  from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401
193
234
  from tqdm import tqdm # noqa isort:skip pylint: disable=C0415 disable=E0401
@@ -200,7 +241,7 @@ def __fetch_model_as_snapshot(
200
241
  workers = download_options.pop("max_workers", 2)
201
242
  cache = download_options.pop("cache_dir", persistent_storage())
202
243
  return snapshot_download(
203
- mode_name, tqdm_class=TqdmProgress, **download_options, max_workers=workers, cache_dir=cache
244
+ model_name, tqdm_class=TqdmProgress, **download_options, max_workers=workers, cache_dir=cache
204
245
  )
205
246
 
206
247
 
nc_py_api/files/_files.py CHANGED
@@ -9,7 +9,7 @@ from urllib.parse import unquote
9
9
  from xml.etree import ElementTree
10
10
 
11
11
  import xmltodict
12
- from httpx import Response
12
+ from niquests import Response
13
13
 
14
14
  from .._exceptions import NextcloudException, check_error
15
15
  from .._misc import check_capabilities, clear_from_params_empty
nc_py_api/files/files.py CHANGED
@@ -5,7 +5,7 @@ import os
5
5
  from pathlib import Path
6
6
  from urllib.parse import quote
7
7
 
8
- from httpx import Headers
8
+ from niquests.structures import CaseInsensitiveDict
9
9
 
10
10
  from .._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
11
11
  from .._misc import random_string, require_capabilities
@@ -80,7 +80,7 @@ class FilesAPI:
80
80
  # `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
81
81
  root = build_find_request(req, path, self._session.user, self._session.capabilities)
82
82
  webdav_response = self._session.adapter_dav.request(
83
- "SEARCH", "", content=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
83
+ "SEARCH", "", data=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
84
84
  )
85
85
  request_info = f"find: {self._session.user}, {req}, {path}"
86
86
  return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
@@ -133,7 +133,7 @@ class FilesAPI:
133
133
  """
134
134
  path = path.user_path if isinstance(path, FsNode) else path
135
135
  full_path = dav_get_obj_path(self._session.user, path)
136
- response = self._session.adapter_dav.put(quote(full_path), content=content)
136
+ response = self._session.adapter_dav.put(quote(full_path), data=content)
137
137
  check_error(response, f"upload: user={self._session.user}, path={path}, size={len(content)}")
138
138
  return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
139
139
 
@@ -215,7 +215,7 @@ class FilesAPI:
215
215
  self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
216
216
  )
217
217
  dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
218
- headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
218
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
219
219
  response = self._session.adapter_dav.request(
220
220
  "MOVE",
221
221
  quote(dav_get_obj_path(self._session.user, path_src)),
@@ -237,7 +237,7 @@ class FilesAPI:
237
237
  self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
238
238
  )
239
239
  dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
240
- headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
240
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
241
241
  response = self._session.adapter_dav.request(
242
242
  "COPY",
243
243
  quote(dav_get_obj_path(self._session.user, path_src)),
@@ -257,7 +257,7 @@ class FilesAPI:
257
257
  """
258
258
  root = build_list_by_criteria_req(properties, tags, self._session.capabilities)
259
259
  webdav_response = self._session.adapter_dav.request(
260
- "REPORT", dav_get_obj_path(self._session.user), content=element_tree_as_str(root)
260
+ "REPORT", dav_get_obj_path(self._session.user), data=element_tree_as_str(root)
261
261
  )
262
262
  request_info = f"list_files_by_criteria: {self._session.user}"
263
263
  check_error(webdav_response, request_info)
@@ -272,7 +272,7 @@ class FilesAPI:
272
272
  path = path.user_path if isinstance(path, FsNode) else path
273
273
  root = build_setfav_req(value)
274
274
  webdav_response = self._session.adapter_dav.request(
275
- "PROPPATCH", quote(dav_get_obj_path(self._session.user, path)), content=element_tree_as_str(root)
275
+ "PROPPATCH", quote(dav_get_obj_path(self._session.user, path)), data=element_tree_as_str(root)
276
276
  )
277
277
  check_error(webdav_response, f"setfav: path={path}, value={value}")
278
278
 
@@ -293,7 +293,7 @@ class FilesAPI:
293
293
  path = path.user_path if isinstance(path, FsNode) else path
294
294
 
295
295
  dest = self._session.cfg.dav_endpoint + f"/trashbin/{self._session.user}/restore/{restore_name}"
296
- headers = Headers({"Destination": dest}, encoding="utf-8")
296
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
297
297
  response = self._session.adapter_dav.request(
298
298
  "MOVE",
299
299
  quote(f"/trashbin/{self._session.user}/{path}"),
@@ -336,7 +336,7 @@ class FilesAPI:
336
336
  """
337
337
  require_capabilities("files.versioning", self._session.capabilities)
338
338
  dest = self._session.cfg.dav_endpoint + f"/versions/{self._session.user}/restore/{file_object.name}"
339
- headers = Headers({"Destination": dest}, encoding="utf-8")
339
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
340
340
  response = self._session.adapter_dav.request(
341
341
  "MOVE",
342
342
  quote(f"/versions/{self._session.user}/{file_object.user_path}"),
@@ -347,7 +347,7 @@ class FilesAPI:
347
347
  def list_tags(self) -> list[SystemTag]:
348
348
  """Returns list of the avalaible Tags."""
349
349
  root = build_list_tag_req()
350
- response = self._session.adapter_dav.request("PROPFIND", "/systemtags", content=element_tree_as_str(root))
350
+ response = self._session.adapter_dav.request("PROPFIND", "/systemtags", data=element_tree_as_str(root))
351
351
  return build_list_tags_response(response)
352
352
 
353
353
  def get_tags(self, file_id: FsNode | int) -> list[SystemTag]:
@@ -389,7 +389,7 @@ class FilesAPI:
389
389
  tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
390
390
  root = build_update_tag_req(name, user_visible, user_assignable)
391
391
  response = self._session.adapter_dav.request(
392
- "PROPPATCH", f"/systemtags/{tag_id}", content=element_tree_as_str(root)
392
+ "PROPPATCH", f"/systemtags/{tag_id}", data=element_tree_as_str(root)
393
393
  )
394
394
  check_error(response)
395
395
 
@@ -466,7 +466,7 @@ class FilesAPI:
466
466
  webdav_response = self._session.adapter_dav.request(
467
467
  "PROPFIND",
468
468
  quote(dav_path),
469
- content=element_tree_as_str(root),
469
+ data=element_tree_as_str(root),
470
470
  headers={"Depth": "infinity" if depth == -1 else str(depth)},
471
471
  )
472
472
  return build_listdir_response(
@@ -478,7 +478,9 @@ class FilesAPI:
478
478
  _dav_path = quote(dav_get_obj_path(self._session.user, _tmp_path, root_path="/uploads"))
479
479
  _v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
480
480
  full_path = dav_get_obj_path(self._session.user, path)
481
- headers = Headers({"Destination": self._session.cfg.dav_endpoint + quote(full_path)}, encoding="utf-8")
481
+ headers = CaseInsensitiveDict(
482
+ {"Destination": (self._session.cfg.dav_endpoint + quote(full_path)).encode("utf-8")}
483
+ )
482
484
  if _v2:
483
485
  response = self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
484
486
  else:
@@ -494,11 +496,11 @@ class FilesAPI:
494
496
  end_bytes = start_bytes + len(piece)
495
497
  if _v2:
496
498
  response = self._session.adapter_dav.put(
497
- _dav_path + "/" + str(chunk_number), content=piece, headers=headers
499
+ _dav_path + "/" + str(chunk_number), data=piece, headers=headers
498
500
  )
499
501
  else:
500
502
  _filename = str(start_bytes).rjust(15, "0") + "-" + str(end_bytes).rjust(15, "0")
501
- response = self._session.adapter_dav.put(_dav_path + "/" + _filename, content=piece)
503
+ response = self._session.adapter_dav.put(_dav_path + "/" + _filename, data=piece)
502
504
  check_error(
503
505
  response,
504
506
  f"upload_stream(v={_v2}): user={self._session.user}, path={path}, cur_size={end_bytes}",
@@ -5,7 +5,7 @@ import os
5
5
  from pathlib import Path
6
6
  from urllib.parse import quote
7
7
 
8
- from httpx import Headers
8
+ from niquests.structures import CaseInsensitiveDict
9
9
 
10
10
  from .._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
11
11
  from .._misc import random_string, require_capabilities
@@ -82,7 +82,7 @@ class AsyncFilesAPI:
82
82
  # `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
83
83
  root = build_find_request(req, path, await self._session.user, await self._session.capabilities)
84
84
  webdav_response = await self._session.adapter_dav.request(
85
- "SEARCH", "", content=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
85
+ "SEARCH", "", data=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
86
86
  )
87
87
  request_info = f"find: {await self._session.user}, {req}, {path}"
88
88
  return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
@@ -137,7 +137,7 @@ class AsyncFilesAPI:
137
137
  """
138
138
  path = path.user_path if isinstance(path, FsNode) else path
139
139
  full_path = dav_get_obj_path(await self._session.user, path)
140
- response = await self._session.adapter_dav.put(quote(full_path), content=content)
140
+ response = await self._session.adapter_dav.put(quote(full_path), data=content)
141
141
  check_error(response, f"upload: user={await self._session.user}, path={path}, size={len(content)}")
142
142
  return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
143
143
 
@@ -219,7 +219,7 @@ class AsyncFilesAPI:
219
219
  await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
220
220
  )
221
221
  dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
222
- headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
222
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
223
223
  response = await self._session.adapter_dav.request(
224
224
  "MOVE",
225
225
  quote(dav_get_obj_path(await self._session.user, path_src)),
@@ -241,7 +241,7 @@ class AsyncFilesAPI:
241
241
  await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
242
242
  )
243
243
  dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
244
- headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
244
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8"), "Overwrite": "T" if overwrite else "F"})
245
245
  response = await self._session.adapter_dav.request(
246
246
  "COPY",
247
247
  quote(dav_get_obj_path(await self._session.user, path_src)),
@@ -261,7 +261,7 @@ class AsyncFilesAPI:
261
261
  """
262
262
  root = build_list_by_criteria_req(properties, tags, await self._session.capabilities)
263
263
  webdav_response = await self._session.adapter_dav.request(
264
- "REPORT", dav_get_obj_path(await self._session.user), content=element_tree_as_str(root)
264
+ "REPORT", dav_get_obj_path(await self._session.user), data=element_tree_as_str(root)
265
265
  )
266
266
  request_info = f"list_files_by_criteria: {await self._session.user}"
267
267
  check_error(webdav_response, request_info)
@@ -276,7 +276,7 @@ class AsyncFilesAPI:
276
276
  path = path.user_path if isinstance(path, FsNode) else path
277
277
  root = build_setfav_req(value)
278
278
  webdav_response = await self._session.adapter_dav.request(
279
- "PROPPATCH", quote(dav_get_obj_path(await self._session.user, path)), content=element_tree_as_str(root)
279
+ "PROPPATCH", quote(dav_get_obj_path(await self._session.user, path)), data=element_tree_as_str(root)
280
280
  )
281
281
  check_error(webdav_response, f"setfav: path={path}, value={value}")
282
282
 
@@ -302,7 +302,7 @@ class AsyncFilesAPI:
302
302
  path = path.user_path if isinstance(path, FsNode) else path
303
303
 
304
304
  dest = self._session.cfg.dav_endpoint + f"/trashbin/{await self._session.user}/restore/{restore_name}"
305
- headers = Headers({"Destination": dest}, encoding="utf-8")
305
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
306
306
  response = await self._session.adapter_dav.request(
307
307
  "MOVE",
308
308
  quote(f"/trashbin/{await self._session.user}/{path}"),
@@ -345,7 +345,7 @@ class AsyncFilesAPI:
345
345
  """
346
346
  require_capabilities("files.versioning", await self._session.capabilities)
347
347
  dest = self._session.cfg.dav_endpoint + f"/versions/{await self._session.user}/restore/{file_object.name}"
348
- headers = Headers({"Destination": dest}, encoding="utf-8")
348
+ headers = CaseInsensitiveDict({"Destination": dest.encode("utf-8")})
349
349
  response = await self._session.adapter_dav.request(
350
350
  "MOVE",
351
351
  quote(f"/versions/{await self._session.user}/{file_object.user_path}"),
@@ -356,7 +356,7 @@ class AsyncFilesAPI:
356
356
  async def list_tags(self) -> list[SystemTag]:
357
357
  """Returns list of the avalaible Tags."""
358
358
  root = build_list_tag_req()
359
- response = await self._session.adapter_dav.request("PROPFIND", "/systemtags", content=element_tree_as_str(root))
359
+ response = await self._session.adapter_dav.request("PROPFIND", "/systemtags", data=element_tree_as_str(root))
360
360
  return build_list_tags_response(response)
361
361
 
362
362
  async def get_tags(self, file_id: FsNode | int) -> list[SystemTag]:
@@ -398,7 +398,7 @@ class AsyncFilesAPI:
398
398
  tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
399
399
  root = build_update_tag_req(name, user_visible, user_assignable)
400
400
  response = await self._session.adapter_dav.request(
401
- "PROPPATCH", f"/systemtags/{tag_id}", content=element_tree_as_str(root)
401
+ "PROPPATCH", f"/systemtags/{tag_id}", data=element_tree_as_str(root)
402
402
  )
403
403
  check_error(response)
404
404
 
@@ -435,7 +435,7 @@ class AsyncFilesAPI:
435
435
  quote(full_path),
436
436
  headers={"X-User-Lock": "1", "X-User-Lock-Type": str(lock_type.value)},
437
437
  )
438
- check_error(response, f"lock: user={self._session.user}, path={full_path}")
438
+ check_error(response, f"lock: user={await self._session.user}, path={full_path}")
439
439
 
440
440
  async def unlock(self, path: FsNode | str) -> None:
441
441
  """Unlocks the file.
@@ -449,7 +449,7 @@ class AsyncFilesAPI:
449
449
  quote(full_path),
450
450
  headers={"X-User-Lock": "1"},
451
451
  )
452
- check_error(response, f"unlock: user={self._session.user}, path={full_path}")
452
+ check_error(response, f"unlock: user={await self._session.user}, path={full_path}")
453
453
 
454
454
  async def _file_change_tag_state(self, file_id: FsNode | int, tag_id: SystemTag | int, tag_state: bool) -> None:
455
455
  fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
@@ -475,7 +475,7 @@ class AsyncFilesAPI:
475
475
  webdav_response = await self._session.adapter_dav.request(
476
476
  "PROPFIND",
477
477
  quote(dav_path),
478
- content=element_tree_as_str(root),
478
+ data=element_tree_as_str(root),
479
479
  headers={"Depth": "infinity" if depth == -1 else str(depth)},
480
480
  )
481
481
  return build_listdir_response(
@@ -487,7 +487,9 @@ class AsyncFilesAPI:
487
487
  _dav_path = quote(dav_get_obj_path(await self._session.user, _tmp_path, root_path="/uploads"))
488
488
  _v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
489
489
  full_path = dav_get_obj_path(await self._session.user, path)
490
- headers = Headers({"Destination": self._session.cfg.dav_endpoint + quote(full_path)}, encoding="utf-8")
490
+ headers = CaseInsensitiveDict(
491
+ {"Destination": (self._session.cfg.dav_endpoint + quote(full_path)).encode("utf-8")}
492
+ )
491
493
  if _v2:
492
494
  response = await self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
493
495
  else:
@@ -502,11 +504,11 @@ class AsyncFilesAPI:
502
504
  end_bytes = start_bytes + len(piece)
503
505
  if _v2:
504
506
  response = await self._session.adapter_dav.put(
505
- _dav_path + "/" + str(chunk_number), content=piece, headers=headers
507
+ _dav_path + "/" + str(chunk_number), data=piece, headers=headers
506
508
  )
507
509
  else:
508
510
  _filename = str(start_bytes).rjust(15, "0") + "-" + str(end_bytes).rjust(15, "0")
509
- response = await self._session.adapter_dav.put(_dav_path + "/" + _filename, content=piece)
511
+ response = await self._session.adapter_dav.put(_dav_path + "/" + _filename, data=piece)
510
512
  check_error(
511
513
  response,
512
514
  f"upload_stream(v={_v2}): user={await self._session.user}, path={path}, cur_size={end_bytes}",
nc_py_api/loginflow_v2.py CHANGED
@@ -5,7 +5,7 @@ import json
5
5
  import time
6
6
  from dataclasses import dataclass
7
7
 
8
- import httpx
8
+ import niquests
9
9
 
10
10
  from ._exceptions import check_error
11
11
  from ._session import AsyncNcSession, NcSession
@@ -156,6 +156,6 @@ class _AsyncLoginFlowV2API:
156
156
  return r_model
157
157
 
158
158
 
159
- def _res_to_json(response: httpx.Response) -> dict:
159
+ def _res_to_json(response: niquests.Response) -> dict:
160
160
  check_error(response)
161
161
  return json.loads(response.text)
nc_py_api/nextcloud.py CHANGED
@@ -4,7 +4,7 @@ import contextlib
4
4
  import typing
5
5
  from abc import ABC
6
6
 
7
- from httpx import Headers
7
+ from niquests.structures import CaseInsensitiveDict
8
8
 
9
9
  from ._exceptions import NextcloudExceptionNotFound
10
10
  from ._misc import check_capabilities, require_capabilities
@@ -112,8 +112,8 @@ class _NextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
112
112
  self._session.update_server_info()
113
113
 
114
114
  @property
115
- def response_headers(self) -> Headers:
116
- """Returns the `HTTPX headers <https://www.python-httpx.org/api/#headers>`_ from the last response."""
115
+ def response_headers(self) -> CaseInsensitiveDict:
116
+ """Returns the `Niquests headers <https://www.python-httpx.org/api/#headers>`_ from the last response."""
117
117
  return self._session.response_headers
118
118
 
119
119
  @property
@@ -216,8 +216,8 @@ class _AsyncNextcloudBasic(ABC): # pylint: disable=too-many-instance-attributes
216
216
  await self._session.update_server_info()
217
217
 
218
218
  @property
219
- def response_headers(self) -> Headers:
220
- """Returns the `HTTPX headers <https://www.python-httpx.org/api/#headers>`_ from the last response."""
219
+ def response_headers(self) -> CaseInsensitiveDict:
220
+ """Returns the `Niquests headers <https://www.python-httpx.org/api/#headers>`_ from the last response."""
221
221
  return self._session.response_headers
222
222
 
223
223
  @property
nc_py_api/notes.py CHANGED
@@ -5,7 +5,7 @@ import datetime
5
5
  import json
6
6
  import typing
7
7
 
8
- import httpx
8
+ import niquests
9
9
 
10
10
  from ._exceptions import check_error
11
11
  from ._misc import check_capabilities, clear_from_params_empty, require_capabilities
@@ -367,6 +367,6 @@ class _AsyncNotesAPI:
367
367
  check_error(await self._session.adapter.put(self._ep_base + "/settings", json=params))
368
368
 
369
369
 
370
- def _res_to_json(response: httpx.Response) -> dict:
370
+ def _res_to_json(response: niquests.Response) -> dict:
371
371
  check_error(response)
372
372
  return json.loads(response.text) if response.status_code != 304 else {}
nc_py_api/options.py CHANGED
@@ -33,22 +33,13 @@ NPA_NC_CERT: bool | str
33
33
  SSL certificates (a.k.a CA bundle) used to verify the identity of requested hosts. Either **True** (default CA bundle),
34
34
  a path to an SSL certificate file, or **False** (which will disable verification)."""
35
35
  str_val = environ.get("NPA_NC_CERT", "True")
36
- # https://github.com/encode/httpx/issues/302
37
- # when "httpx" will switch to use "truststore" by default - uncomment next line
38
- # NPA_NC_CERT = True
36
+
37
+ NPA_NC_CERT = True
38
+
39
39
  if str_val.lower() in ("false", "0"):
40
40
  NPA_NC_CERT = False
41
41
  elif str_val.lower() not in ("true", "1"):
42
42
  NPA_NC_CERT = str_val
43
- else:
44
- # Temporary workaround, see comment above.
45
- # Use system certificate stores
46
-
47
- import ssl
48
-
49
- import truststore
50
-
51
- NPA_NC_CERT = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
52
43
 
53
44
  CHUNKED_UPLOAD_V2 = environ.get("CHUNKED_UPLOAD_V2", True)
54
45
  """Option to enable/disable **version 2** chunked upload(better Object Storages support).
nc_py_api/talk_bot.py CHANGED
@@ -7,7 +7,7 @@ import json
7
7
  import os
8
8
  import typing
9
9
 
10
- import httpx
10
+ import niquests
11
11
 
12
12
  from . import options
13
13
  from ._misc import random_string
@@ -117,7 +117,7 @@ class TalkBot:
117
117
 
118
118
  def send_message(
119
119
  self, message: str, reply_to_message: int | TalkBotMessage, silent: bool = False, token: str = ""
120
- ) -> tuple[httpx.Response, str]:
120
+ ) -> tuple[niquests.Response, str]:
121
121
  """Send a message and returns a "reference string" to identify the message again in a "get messages" request.
122
122
 
123
123
  :param message: The message to say.
@@ -127,7 +127,7 @@ class TalkBot:
127
127
  :param silent: Flag controlling if the message should create a chat notifications for the users.
128
128
  :param token: Token of the conversation.
129
129
  Can be empty if ``reply_to_message`` is :py:class:`~nc_py_api.talk_bot.TalkBotMessage`.
130
- :returns: Tuple, where fist element is :py:class:`httpx.Response` and second is a "reference string".
130
+ :returns: Tuple, where fist element is :py:class:`niquests.Response` and second is a "reference string".
131
131
  :raises ValueError: in case of an invalid usage.
132
132
  :raises RuntimeError: in case of a broken installation.
133
133
  """
@@ -143,7 +143,7 @@ class TalkBot:
143
143
  }
144
144
  return self._sign_send_request("POST", f"/{token}/message", params, message), reference_id
145
145
 
146
- def react_to_message(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> httpx.Response:
146
+ def react_to_message(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> niquests.Response:
147
147
  """React to a message.
148
148
 
149
149
  :param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to react to.
@@ -162,7 +162,7 @@ class TalkBot:
162
162
  }
163
163
  return self._sign_send_request("POST", f"/{token}/reaction/{message_id}", params, reaction)
164
164
 
165
- def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> httpx.Response:
165
+ def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> niquests.Response:
166
166
  """Removes reaction from a message.
167
167
 
168
168
  :param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to remove reaction from.
@@ -181,7 +181,7 @@ class TalkBot:
181
181
  }
182
182
  return self._sign_send_request("DELETE", f"/{token}/reaction/{message_id}", params, reaction)
183
183
 
184
- def _sign_send_request(self, method: str, url_suffix: str, data: dict, data_to_sign: str) -> httpx.Response:
184
+ def _sign_send_request(self, method: str, url_suffix: str, data: dict, data_to_sign: str) -> niquests.Response:
185
185
  secret = get_bot_secret(self.callback_url)
186
186
  if secret is None:
187
187
  raise RuntimeError("Can't find the 'secret' of the bot. Has the bot been installed?")
@@ -189,7 +189,8 @@ class TalkBot:
189
189
  hmac_sign = hmac.new(secret, talk_bot_random.encode("UTF-8"), digestmod=hashlib.sha256)
190
190
  hmac_sign.update(data_to_sign.encode("UTF-8"))
191
191
  nc_app_cfg = BasicConfig()
192
- with httpx.Client(verify=nc_app_cfg.options.nc_cert) as client:
192
+ with niquests.Session() as client:
193
+ client.verify = nc_app_cfg.options.nc_cert
193
194
  return client.request(
194
195
  method,
195
196
  url=nc_app_cfg.endpoint + "/ocs/v2.php/apps/spreed/api/v1/bot" + url_suffix,
@@ -234,7 +235,7 @@ class AsyncTalkBot:
234
235
 
235
236
  async def send_message(
236
237
  self, message: str, reply_to_message: int | TalkBotMessage, silent: bool = False, token: str = ""
237
- ) -> tuple[httpx.Response, str]:
238
+ ) -> tuple[niquests.Response, str]:
238
239
  """Send a message and returns a "reference string" to identify the message again in a "get messages" request.
239
240
 
240
241
  :param message: The message to say.
@@ -244,7 +245,7 @@ class AsyncTalkBot:
244
245
  :param silent: Flag controlling if the message should create a chat notifications for the users.
245
246
  :param token: Token of the conversation.
246
247
  Can be empty if ``reply_to_message`` is :py:class:`~nc_py_api.talk_bot.TalkBotMessage`.
247
- :returns: Tuple, where fist element is :py:class:`httpx.Response` and second is a "reference string".
248
+ :returns: Tuple, where fist element is :py:class:`niquests.Response` and second is a "reference string".
248
249
  :raises ValueError: in case of an invalid usage.
249
250
  :raises RuntimeError: in case of a broken installation.
250
251
  """
@@ -260,7 +261,9 @@ class AsyncTalkBot:
260
261
  }
261
262
  return await self._sign_send_request("POST", f"/{token}/message", params, message), reference_id
262
263
 
263
- async def react_to_message(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> httpx.Response:
264
+ async def react_to_message(
265
+ self, message: int | TalkBotMessage, reaction: str, token: str = ""
266
+ ) -> niquests.Response:
264
267
  """React to a message.
265
268
 
266
269
  :param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to react to.
@@ -279,7 +282,7 @@ class AsyncTalkBot:
279
282
  }
280
283
  return await self._sign_send_request("POST", f"/{token}/reaction/{message_id}", params, reaction)
281
284
 
282
- async def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> httpx.Response:
285
+ async def delete_reaction(self, message: int | TalkBotMessage, reaction: str, token: str = "") -> niquests.Response:
283
286
  """Removes reaction from a message.
284
287
 
285
288
  :param message: Message ID or :py:class:`~nc_py_api.talk_bot.TalkBotMessage` to remove reaction from.
@@ -298,7 +301,9 @@ class AsyncTalkBot:
298
301
  }
299
302
  return await self._sign_send_request("DELETE", f"/{token}/reaction/{message_id}", params, reaction)
300
303
 
301
- async def _sign_send_request(self, method: str, url_suffix: str, data: dict, data_to_sign: str) -> httpx.Response:
304
+ async def _sign_send_request(
305
+ self, method: str, url_suffix: str, data: dict, data_to_sign: str
306
+ ) -> niquests.Response:
302
307
  secret = await aget_bot_secret(self.callback_url)
303
308
  if secret is None:
304
309
  raise RuntimeError("Can't find the 'secret' of the bot. Has the bot been installed?")
@@ -306,7 +311,9 @@ class AsyncTalkBot:
306
311
  hmac_sign = hmac.new(secret, talk_bot_random.encode("UTF-8"), digestmod=hashlib.sha256)
307
312
  hmac_sign.update(data_to_sign.encode("UTF-8"))
308
313
  nc_app_cfg = BasicConfig()
309
- async with httpx.AsyncClient(verify=nc_app_cfg.options.nc_cert) as aclient:
314
+ async with niquests.AsyncSession() as aclient:
315
+ aclient.verify = nc_app_cfg.options.nc_cert
316
+
310
317
  return await aclient.request(
311
318
  method,
312
319
  url=nc_app_cfg.endpoint + "/ocs/v2.php/apps/spreed/api/v1/bot" + url_suffix,
nc_py_api/users.py CHANGED
@@ -207,6 +207,10 @@ class _UsersAPI:
207
207
  """Disables user on the Nextcloud server."""
208
208
  self._session.ocs("PUT", f"{self._ep_base}/{user_id}/disable")
209
209
 
210
+ def wipe(self, user_id: str) -> None:
211
+ """Disconnects user from the Nextcloud server and deletes all local data stored on the user's devices."""
212
+ self._session.ocs("POST", f"/ocs/v2.php/cloud/users/{user_id}/wipe")
213
+
210
214
  def resend_welcome_email(self, user_id: str) -> None:
211
215
  """Send welcome email for specified user again."""
212
216
  self._session.ocs("POST", f"{self._ep_base}/{user_id}/welcome")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-py-api
3
- Version: 0.20.2
3
+ Version: 0.21.1
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/
@@ -31,10 +31,9 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
31
31
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
32
32
  Requires-Python: >=3.10
33
33
  Requires-Dist: fastapi>=0.109.2
34
- Requires-Dist: httpx>=0.25.2
34
+ Requires-Dist: niquests<4,>=3
35
35
  Requires-Dist: pydantic>=2.1.1
36
36
  Requires-Dist: python-dotenv>=1
37
- Requires-Dist: truststore==0.10
38
37
  Requires-Dist: xmltodict>=0.13
39
38
  Provides-Extra: app
40
39
  Requires-Dist: uvicorn[standard]>=0.23.2; extra == 'app'
@@ -1,31 +1,31 @@
1
- nc_py_api/__init__.py,sha256=FOju0DYizwYYzWBgID8Ebx0GeId-faNqd4Z_3K0216o,439
1
+ nc_py_api/__init__.py,sha256=S5IytGqp8PkWnmcZXRFPSVEwfeb4iZ8xPeCmSf4sTFQ,460
2
2
  nc_py_api/_deffered_error.py,sha256=BpEe_tBqflwfj2Zolb67nhW-K16XX-WbcY2IH_6u8fo,319
3
- nc_py_api/_exceptions.py,sha256=7vbUECaLmD7RJBCU27t4fuP6NmQK6r4508u_gS4szhI,2298
3
+ nc_py_api/_exceptions.py,sha256=_4k-uzpKdW5_WjqVE_saoGFJ1YqWx7W6kr-wgpBume0,2414
4
4
  nc_py_api/_misc.py,sha256=dUzCP9VmyhtICTsn1aexlFAYUioBm40k6Zh-YE5WwCY,3333
5
5
  nc_py_api/_preferences.py,sha256=OtovFZuGHnHYKjdDjSnUappO795tW8Oxj7qVaejHWpQ,2479
6
6
  nc_py_api/_preferences_ex.py,sha256=Y6sDBrFJc7lk8BoDUfjC_iwOfjSbPPNPpcSxsL1fyIM,6391
7
- nc_py_api/_session.py,sha256=8CjrT0ZDTg8cugRkukxUHGA_O7UsiwleawZ_KyEI6n0,20664
7
+ nc_py_api/_session.py,sha256=u45U4USdgbegBj3oAnm-OErFng2fAyleH1dWgshJSso,22018
8
8
  nc_py_api/_talk_api.py,sha256=0Uo7OduYniuuX3UQPb468RyGJJ-PWBCgJ5HoPuz5Qa0,51068
9
9
  nc_py_api/_theming.py,sha256=hTr3nuOemSuRFZaPy9iXNmBM7rDgQHECH43tHMWGqEY,1870
10
- nc_py_api/_version.py,sha256=rf1wT5GKIXVuBGABQqd82PICMV7siLu9r7a1l9gHq4E,52
10
+ nc_py_api/_version.py,sha256=qWD6wPKnxx25NaWUPbzrV-OCufP6NS2579oj1jEc-ak,52
11
11
  nc_py_api/activity.py,sha256=t9VDSnnaXRNOvALqOSGCeXSQZ-426pCOMSfQ96JHys4,9574
12
12
  nc_py_api/apps.py,sha256=Us2y2lszdxXlD8t6kxwd5_Nrrmazc0EvZXIH9O-ol80,9315
13
- nc_py_api/calendar_api.py,sha256=-T6CJ8cRbJZTLtxSEDWuuYpD29DMJGCTfLONmtxZV9w,1445
14
- nc_py_api/loginflow_v2.py,sha256=UjkK3gMouzNrIS7BFhjbmB_9m4fP2UY5sfpmvJ2EXWk,5760
15
- nc_py_api/nextcloud.py,sha256=C3lnns8Dx34wXsOoN4vwOc0Sm5rA8CgH4UtxNNKFyKc,22793
16
- nc_py_api/notes.py,sha256=ljO3TOe-Qg0bJ0mlFQwjg--Pxmj-XFknoLbcbJmII0A,15106
13
+ nc_py_api/calendar_api.py,sha256=a2Q5EGf5_swWPYkUbHnoEg6h1S9KTEUQD7f7DljGHYY,1442
14
+ nc_py_api/loginflow_v2.py,sha256=QgR99Q59Q1My5U_PeLFkIAvEKhX_H7bIRrBZdddvmo4,5766
15
+ nc_py_api/nextcloud.py,sha256=cKdsw0n_yFNdI-N8IIVIQKkSqmEzJnQtLBVqo24EVdM,22849
16
+ nc_py_api/notes.py,sha256=aM0SLVGKv8nBv_qI3z8sN08Z2721wLJUmEdwlo2EK1g,15112
17
17
  nc_py_api/notifications.py,sha256=WgzV21TuLOhLk-UEjhBSbMsIi2isa5MmAx6cbe0pc2Y,9187
18
- nc_py_api/options.py,sha256=ZKJqVLhWVwJ6YxAKzGWlPXJhd1vg9GluyFzr-jX4Jp0,2038
18
+ nc_py_api/options.py,sha256=W9RSLTtltW1W-Y3iSllRr2-VikFB5wARSiBEaAcw-AI,1719
19
19
  nc_py_api/talk.py,sha256=OZFemYkDOaM6o4xAK3EvQbjMFiK75E5qnsCDyihIElg,29368
20
- nc_py_api/talk_bot.py,sha256=_RuImwb3jYvUKX3ywcX095ucOjECCxsuc59heIxNoTM,16725
20
+ nc_py_api/talk_bot.py,sha256=QXaKKwIRRcOAnqoJgfzK3ub0aDPTUmTkagls-a0XZW0,16840
21
21
  nc_py_api/user_status.py,sha256=I101nwYS8X1WvC8AnLa2f3qJUCPDPHrbq-ke0h1VT4E,13282
22
- nc_py_api/users.py,sha256=SQG8Agplaxy7XJgguK-rxV-azpc-QdktbDmLmQtJkXo,15476
22
+ nc_py_api/users.py,sha256=_TUot3XdI2VDAwpUo33IOG_JsrgOrYzfY6KJRsWQcD0,15710
23
23
  nc_py_api/users_groups.py,sha256=IPxw-Ks5NjCm6r8_HC9xmf3IYptH00ulITbp5iazhAo,6289
24
24
  nc_py_api/weather_status.py,sha256=wAkjuJPjxc0Rxe4za0BzfwB0XeUmkCXoisJtTH3-qdQ,7582
25
25
  nc_py_api/webhooks.py,sha256=BGHRtankgbUkcqBRJTFShjRLpaVoFNcjLsrVitoNziM,8083
26
26
  nc_py_api/ex_app/__init__.py,sha256=6Lwid4bBXOSrZf_ocf5m8qkkO1OgYxG0GTs4q6Nw72o,691
27
27
  nc_py_api/ex_app/defs.py,sha256=FaQInH3jLugKxDUqpwrXdkMT-lBxmoqWmXJXc11fa6A,727
28
- nc_py_api/ex_app/integration_fastapi.py,sha256=848ugedpEsQ-0vGPLfF5VlgQxETPMQl_aQN-erJf4CE,11058
28
+ nc_py_api/ex_app/integration_fastapi.py,sha256=mWMG8VrX_Q9yFw5hA3Wp996lyrybPnxTIT8sxp4GBrk,12636
29
29
  nc_py_api/ex_app/logger.py,sha256=nAHLObuPvl3UBLrlqZulgoxxVaAJ661iP4F6bTW-V-Y,1475
30
30
  nc_py_api/ex_app/misc.py,sha256=c7B0uE8isaIi4SQbxURGUuWjZaaXiLg3Ov6cqvRYplE,2298
31
31
  nc_py_api/ex_app/occ_commands.py,sha256=hb2BJuvFKIigvLycSCyAe9v6hedq4Gfu2junQZTaK_M,5219
@@ -41,12 +41,12 @@ nc_py_api/ex_app/ui/settings.py,sha256=VyGly-rCYMw9Zw7cTYRYilXXKe7f6tesTAW83X5gH
41
41
  nc_py_api/ex_app/ui/top_menu.py,sha256=oCgGtIoMYbp-5iN5aXEbT7Q88HtccR7hg6IBFgbbyX4,5118
42
42
  nc_py_api/ex_app/ui/ui.py,sha256=OqFHKn6oIZli8T1wnv6YtQ4glNfeNb90WwGCvtWI1Z4,1632
43
43
  nc_py_api/files/__init__.py,sha256=aN6Lhc0km5eAIGFA8YoG-xXDOzIJILV2izCzHeC-a78,17949
44
- nc_py_api/files/_files.py,sha256=0jJ1yoq_wZUiB6pXrEG4iUhImcANEO5gXoqmUrJF_ws,14297
45
- nc_py_api/files/files.py,sha256=7x4hfnVa2y1R5bxK9f8cdggi1gPnpUYnsBj0e4p-4qc,24926
46
- nc_py_api/files/files_async.py,sha256=nyDeWiGx_8CHuVrNThSgfZy0l-WlC4El-TzJONo0TsM,25773
44
+ nc_py_api/files/_files.py,sha256=t7KBZE3fHmfVk0WZuQF9usCMY_b4rzX1ct1aIZx3RYw,14300
45
+ nc_py_api/files/files.py,sha256=A05iT_s5cYMK1MtarqdjTZP9QA1l_SiINq805VLrzuY,24999
46
+ nc_py_api/files/files_async.py,sha256=GoTPswMgozcx3Vkbj4YSitNeP7vpPJ4lIbrCnj77rP4,25858
47
47
  nc_py_api/files/sharing.py,sha256=VRZCl-TYK6dbu9rUHPs3_jcVozu1EO8bLGZwoRpiLsU,14439
48
- nc_py_api-0.20.2.dist-info/METADATA,sha256=ehwNyvLLh9FU2Mq5ykO1hKgiE6dHmDgpQ3agBA6CdxY,8055
49
- nc_py_api-0.20.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
- nc_py_api-0.20.2.dist-info/licenses/AUTHORS,sha256=B2Q9q9XH3PAxJp0V3GiKQc1l0z7vtGDpDHqda-ISWKM,616
51
- nc_py_api-0.20.2.dist-info/licenses/LICENSE.txt,sha256=OLEMh401fAumGHfRSna365MLIfnjdTcdOHZ6QOzMjkg,1551
52
- nc_py_api-0.20.2.dist-info/RECORD,,
48
+ nc_py_api-0.21.1.dist-info/METADATA,sha256=E0ZErYs8U1KOkOVjx_s1Gp4mExqOV6flkh-1Wm63qZI,8024
49
+ nc_py_api-0.21.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
+ nc_py_api-0.21.1.dist-info/licenses/AUTHORS,sha256=B2Q9q9XH3PAxJp0V3GiKQc1l0z7vtGDpDHqda-ISWKM,616
51
+ nc_py_api-0.21.1.dist-info/licenses/LICENSE.txt,sha256=OLEMh401fAumGHfRSna365MLIfnjdTcdOHZ6QOzMjkg,1551
52
+ nc_py_api-0.21.1.dist-info/RECORD,,