pyzotero 1.6.0__py3-none-any.whl → 1.6.2__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.
_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.6.0'
16
- __version_tuple__ = version_tuple = (1, 6, 0)
15
+ __version__ = version = '1.6.2'
16
+ __version_tuple__ = version_tuple = (1, 6, 2)
pyzotero/zotero.py CHANGED
@@ -37,9 +37,9 @@ from urllib.parse import (
37
37
 
38
38
  import bibtexparser
39
39
  import feedparser
40
+ import httpx
40
41
  import pytz
41
- import requests
42
- from requests import Request
42
+ from httpx import Request
43
43
 
44
44
  import pyzotero as pz
45
45
 
@@ -97,11 +97,14 @@ def tcache(func):
97
97
  "GET",
98
98
  build_url(self.endpoint, query_string),
99
99
  params=params,
100
- ).prepare()
100
+ )
101
+ with httpx.Client() as client:
102
+ response = client.send(r)
103
+
101
104
  # now split up the URL
102
- result = urlparse(r.url)
105
+ result = urlparse(str(response.url))
103
106
  # construct cache key
104
- cachekey = result.path + "_" + result.query
107
+ cachekey = f"{result.path}_{result.query}"
105
108
  if self.templates.get(cachekey) and not self._updated(
106
109
  query_string, self.templates[cachekey], cachekey
107
110
  ):
@@ -132,7 +135,7 @@ def backoff_check(func):
132
135
  resp = func(self, *args, **kwargs)
133
136
  try:
134
137
  resp.raise_for_status()
135
- except requests.exceptions.HTTPError as exc:
138
+ except httpx.HTTPError as exc:
136
139
  error_handler(self, resp, exc)
137
140
  self.request = resp
138
141
  backoff = resp.headers.get("backoff") or resp.headers.get("retry-after")
@@ -166,8 +169,8 @@ def retrieve(func):
166
169
  self.links = self._extract_links()
167
170
  # determine content and format, based on url params
168
171
  content = (
169
- self.content.search(self.request.url)
170
- and self.content.search(self.request.url).group(0)
172
+ self.content.search(str(self.request.url))
173
+ and self.content.search(str(self.request.url)).group(0)
171
174
  or "bib"
172
175
  )
173
176
  # JSON by default
@@ -223,7 +226,7 @@ def retrieve(func):
223
226
  file = retrieved.content
224
227
  return file
225
228
  # check to see whether it's tag data
226
- if "tags" in self.request.url:
229
+ if "tags" in str(self.request.url):
227
230
  self.tag_data = False
228
231
  return self._tags_data(retrieved.json())
229
232
  if fmt == "atom":
@@ -277,6 +280,7 @@ class Zotero:
277
280
  locale="en-US",
278
281
  local=False,
279
282
  ):
283
+ self.client = None
280
284
  """Store Zotero credentials"""
281
285
  if not local:
282
286
  self.endpoint = "https://api.zotero.org"
@@ -300,6 +304,7 @@ class Zotero:
300
304
  self.tag_data = False
301
305
  self.request = None
302
306
  self.snapshot = False
307
+ self.client = httpx.Client(headers=self.default_headers())
303
308
  # these aren't valid item fields, so never send them to the server
304
309
  self.temp_keys = set(["key", "etag", "group_id", "updated"])
305
310
  # determine which processor to use for the parsed content
@@ -331,6 +336,11 @@ class Zotero:
331
336
  self.backoff = False
332
337
  self.backoff_duration = 0.0
333
338
 
339
+ def __del__(self):
340
+ # this isn't guaranteed to run, but that's OK
341
+ if c := self.client:
342
+ c.close()
343
+
334
344
  def _check_for_component(self, url, component):
335
345
  """Check a url path query fragment for a specific query parameter"""
336
346
  if parse_qs(url).get(component):
@@ -338,6 +348,7 @@ class Zotero:
338
348
  return False
339
349
 
340
350
  def _striplocal(self, url):
351
+ """We need to remve the leading "/api" substring from urls if we're running in local mode"""
341
352
  if self.local:
342
353
  parsed = urlparse(url)
343
354
  purepath = PurePosixPath(unquote(parsed.path))
@@ -380,11 +391,11 @@ class Zotero:
380
391
  It's always OK to include these headers
381
392
  """
382
393
  _headers = {
383
- "User-Agent": "Pyzotero/%s" % pz.__version__,
384
- "Zotero-API-Version": "%s" % __api_version__,
394
+ "User-Agent": f"Pyzotero/{pz.__version__}",
395
+ "Zotero-API-Version": f"{__api_version__}",
385
396
  }
386
397
  if self.api_key:
387
- _headers["Authorization"] = "Bearer %s" % self.api_key
398
+ _headers["Authorization"] = f"Bearer {self.api_key}"
388
399
  return _headers
389
400
 
390
401
  def _cache(self, response, key):
@@ -432,21 +443,26 @@ class Zotero:
432
443
  # ensure that we wait if there's an active backoff
433
444
  self._check_backoff()
434
445
  # don't set locale if the url already contains it
435
- if params and self.links:
436
- if not self._check_for_component(self.links.get("next"), "locale"):
446
+ # we always add a locale if it's a "standalone" or first call
447
+ needs_locale = not self.links or not self._check_for_component(
448
+ self.links.get("next"), "locale"
449
+ )
450
+ if needs_locale:
451
+ if params:
437
452
  params["locale"] = self.locale
438
- if not params and self.links:
439
- if not self._check_for_component(self.links.get("next"), "locale"):
440
- params = {"locale": self.locale}
441
453
  else:
442
- params = {}
443
- self.request = requests.get(
444
- url=full_url, headers=self.default_headers(), params=params
445
- )
454
+ params = {"locale": self.locale}
455
+ # we now have to merge self.url_params (default params, and those supplied by the user)
456
+ if not params:
457
+ params = {}
458
+ if not self.url_params:
459
+ self.url_params = {}
460
+ merged_params = params | self.url_params
461
+ self.request = self.client.get(url=full_url, params=merged_params)
446
462
  self.request.encoding = "utf-8"
447
463
  try:
448
464
  self.request.raise_for_status()
449
- except requests.exceptions.HTTPError as exc:
465
+ except httpx.HTTPError as exc:
450
466
  error_handler(self, self.request, exc)
451
467
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
452
468
  "retry-after"
@@ -463,7 +479,7 @@ class Zotero:
463
479
  try:
464
480
  for key, value in self.request.links.items():
465
481
  parsed = urlparse(value["url"])
466
- fragment = "{path}?{query}".format(path=parsed[2], query=parsed[4])
482
+ fragment = f"{parsed[2]}?{parsed[4]}"
467
483
  extracted[key] = fragment
468
484
  # add a 'self' link
469
485
  parsed = list(urlparse(self.self_link))
@@ -508,13 +524,12 @@ class Zotero:
508
524
  "%a, %d %b %Y %H:%M:%S %Z"
509
525
  )
510
526
  }
511
- headers.update(self.default_headers())
512
527
  # perform the request, and check whether the response returns 304
513
528
  self._check_backoff()
514
- req = requests.get(query, headers=headers)
529
+ req = self.client.get(query, headers=headers)
515
530
  try:
516
531
  req.raise_for_status()
517
- except requests.exceptions.HTTPError as exc:
532
+ except httpx.HTTPError as exc:
518
533
  error_handler(self, req, exc)
519
534
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
520
535
  "retry-after"
@@ -548,7 +563,7 @@ class Zotero:
548
563
  # bib format can't have a limit
549
564
  if params.get("format") == "bib":
550
565
  del params["limit"]
551
- self.url_params = urlencode(params, doseq=True)
566
+ self.url_params = params
552
567
 
553
568
  def _build_query(self, query_string, no_params=False):
554
569
  """
@@ -558,12 +573,11 @@ class Zotero:
558
573
  try:
559
574
  query = quote(query_string.format(u=self.library_id, t=self.library_type))
560
575
  except KeyError as err:
561
- raise ze.ParamNotPassed("There's a request parameter missing: %s" % err)
576
+ raise ze.ParamNotPassed(f"There's a request parameter missing: {err}")
562
577
  # Add the URL parameters and the user key, if necessary
563
578
  if no_params is False:
564
579
  if not self.url_params:
565
580
  self.add_parameters()
566
- query = "%s?%s" % (query, self.url_params)
567
581
  return query
568
582
 
569
583
  @retrieve
@@ -636,9 +650,9 @@ class Zotero:
636
650
  For text documents, 'indexedChars' and 'totalChars' OR
637
651
  For PDFs, 'indexedPages' and 'totalPages'.
638
652
  """
639
- headers = self.default_headers()
653
+ headers = {}
640
654
  headers.update({"Content-Type": "application/json"})
641
- return requests.put(
655
+ return self.client.put(
642
656
  url=build_url(
643
657
  self.endpoint,
644
658
  "/{t}/{u}/items/{k}/fulltext".format(
@@ -646,7 +660,7 @@ class Zotero:
646
660
  ),
647
661
  ),
648
662
  headers=headers,
649
- data=json.dumps(payload),
663
+ content=json.dumps(payload),
650
664
  )
651
665
 
652
666
  def new_fulltext(self, since):
@@ -654,15 +668,18 @@ class Zotero:
654
668
  Retrieve list of full-text content items and versions which are newer
655
669
  than <since>
656
670
  """
657
- query_string = "/{t}/{u}/fulltext?since={version}".format(
658
- t=self.library_type, u=self.library_id, version=since
671
+ query_string = "/{t}/{u}/fulltext".format(
672
+ t=self.library_type, u=self.library_id
659
673
  )
660
- headers = self.default_headers()
674
+ headers = {}
675
+ params = {"since": since}
661
676
  self._check_backoff()
662
- resp = requests.get(build_url(self.endpoint, query_string), headers=headers)
677
+ resp = self.client.get(
678
+ build_url(self.endpoint, query_string), params=params, headers=headers
679
+ )
663
680
  try:
664
681
  resp.raise_for_status()
665
- except requests.exceptions.HTTPError as exc:
682
+ except httpx.HTTPError as exc:
666
683
  error_handler(self, resp, exc)
667
684
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
668
685
  "retry-after"
@@ -877,6 +894,7 @@ class Zotero:
877
894
  """Return the result of the call to the URL in the 'Next' link"""
878
895
  if n := self.links.get("next"):
879
896
  newurl = self._striplocal(n)
897
+ print(newurl)
880
898
  return newurl
881
899
  return
882
900
 
@@ -981,17 +999,17 @@ class Zotero:
981
999
  def item_template(self, itemtype, linkmode=None):
982
1000
  """Get a template for a new item"""
983
1001
  # if we have a template and it hasn't been updated since we stored it
984
- template_name = "{}_{}_{}".format(*["item_template", itemtype, linkmode or ""])
985
- query_string = "/items/new?itemType={i}".format(i=itemtype)
1002
+ template_name = f"item_template_{itemtype}_{linkmode or ''}"
1003
+ params = {"itemType": itemtype}
1004
+ # Set linkMode parameter for API request if itemtype is attachment
1005
+ if itemtype == "attachment":
1006
+ params["linkMode"] = linkmode
1007
+ self.add_parameters(**params)
1008
+ query_string = "/items/new"
986
1009
  if self.templates.get(template_name) and not self._updated(
987
1010
  query_string, self.templates[template_name], template_name
988
1011
  ):
989
1012
  return copy.deepcopy(self.templates[template_name]["tmplt"])
990
-
991
- # Set linkMode parameter for API request if itemtype is attachment
992
- if itemtype == "attachment":
993
- query_string = "{}&linkMode={}".format(query_string, linkmode)
994
-
995
1013
  # otherwise perform a normal request and cache the response
996
1014
  retrieved = self._retrieve_data(query_string)
997
1015
  return self._cache(retrieved, template_name)
@@ -1047,20 +1065,19 @@ class Zotero:
1047
1065
  self.savedsearch._validate(conditions)
1048
1066
  payload = [{"name": name, "conditions": conditions}]
1049
1067
  headers = {"Zotero-Write-Token": token()}
1050
- headers.update(self.default_headers())
1051
1068
  self._check_backoff()
1052
- req = requests.post(
1069
+ req = self.client.post(
1053
1070
  url=build_url(
1054
1071
  self.endpoint,
1055
1072
  "/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
1056
1073
  ),
1057
1074
  headers=headers,
1058
- data=json.dumps(payload),
1075
+ content=json.dumps(payload),
1059
1076
  )
1060
1077
  self.request = req
1061
1078
  try:
1062
1079
  req.raise_for_status()
1063
- except requests.exceptions.HTTPError as exc:
1080
+ except httpx.HTTPError as exc:
1064
1081
  error_handler(self, req, exc)
1065
1082
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
1066
1083
  "retry-after"
@@ -1075,9 +1092,8 @@ class Zotero:
1075
1092
  unique search keys
1076
1093
  """
1077
1094
  headers = {"Zotero-Write-Token": token()}
1078
- headers.update(self.default_headers())
1079
1095
  self._check_backoff()
1080
- req = requests.delete(
1096
+ req = self.client.delete(
1081
1097
  url=build_url(
1082
1098
  self.endpoint,
1083
1099
  "/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
@@ -1088,7 +1104,7 @@ class Zotero:
1088
1104
  self.request = req
1089
1105
  try:
1090
1106
  req.raise_for_status()
1091
- except requests.exceptions.HTTPError as exc:
1107
+ except httpx.HTTPError as exc:
1092
1108
  error_handler(self, req, exc)
1093
1109
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
1094
1110
  "retry-after"
@@ -1114,7 +1130,7 @@ class Zotero:
1114
1130
  except AssertionError:
1115
1131
  item["data"]["tags"] = list()
1116
1132
  for tag in tags:
1117
- item["data"]["tags"].append({"tag": "%s" % tag})
1133
+ item["data"]["tags"].append({"tag": f"{tag}"})
1118
1134
  # make sure everything's OK
1119
1135
  assert self.check_items([item])
1120
1136
  return self.update_item(item)
@@ -1131,9 +1147,11 @@ class Zotero:
1131
1147
  "GET",
1132
1148
  build_url(self.endpoint, query_string),
1133
1149
  params=params,
1134
- ).prepare()
1150
+ )
1151
+ with httpx.Client() as client:
1152
+ response = client.send(r)
1135
1153
  # now split up the URL
1136
- result = urlparse(r.url)
1154
+ result = urlparse(str(response.url))
1137
1155
  # construct cache key
1138
1156
  cachekey = result.path + "_" + result.query
1139
1157
  if self.templates.get(cachekey) and not self._updated(
@@ -1186,8 +1204,7 @@ class Zotero:
1186
1204
  difference = to_check.difference(template)
1187
1205
  if difference:
1188
1206
  raise ze.InvalidItemFields(
1189
- "Invalid keys present in item %s: %s"
1190
- % (pos + 1, " ".join(i for i in difference))
1207
+ f"Invalid keys present in item {pos + 1}: {' '.join(i for i in difference)}"
1191
1208
  )
1192
1209
  return items
1193
1210
 
@@ -1251,20 +1268,19 @@ class Zotero:
1251
1268
  if last_modified is not None:
1252
1269
  headers["If-Unmodified-Since-Version"] = str(last_modified)
1253
1270
  to_send = json.dumps([i for i in self._cleanup(*payload, allow=("key"))])
1254
- headers.update(self.default_headers())
1255
1271
  self._check_backoff()
1256
- req = requests.post(
1272
+ req = self.client.post(
1257
1273
  url=build_url(
1258
1274
  self.endpoint,
1259
1275
  "/{t}/{u}/items".format(t=self.library_type, u=self.library_id),
1260
1276
  ),
1261
- data=to_send,
1277
+ content=to_send,
1262
1278
  headers=dict(headers),
1263
1279
  )
1264
1280
  self.request = req
1265
1281
  try:
1266
1282
  req.raise_for_status()
1267
- except requests.exceptions.HTTPError as exc:
1283
+ except httpx.HTTPError as exc:
1268
1284
  error_handler(self, req, exc)
1269
1285
  resp = req.json()
1270
1286
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
@@ -1279,24 +1295,23 @@ class Zotero:
1279
1295
  uheaders = {
1280
1296
  "If-Unmodified-Since-Version": req.headers["last-modified-version"]
1281
1297
  }
1282
- uheaders.update(self.default_headers())
1283
1298
  for value in resp["success"].values():
1284
1299
  payload = json.dumps({"parentItem": parentid})
1285
1300
  self._check_backoff()
1286
- presp = requests.patch(
1301
+ presp = self.client.patch(
1287
1302
  url=build_url(
1288
1303
  self.endpoint,
1289
1304
  "/{t}/{u}/items/{v}".format(
1290
1305
  t=self.library_type, u=self.library_id, v=value
1291
1306
  ),
1292
1307
  ),
1293
- data=payload,
1308
+ content=payload,
1294
1309
  headers=dict(uheaders),
1295
1310
  )
1296
1311
  self.request = presp
1297
1312
  try:
1298
1313
  presp.raise_for_status()
1299
- except requests.exceptions.HTTPError as exc:
1314
+ except httpx.HTTPError as exc:
1300
1315
  error_handler(self, presp, exc)
1301
1316
  backoff = presp.headers.get("backoff") or presp.headers.get(
1302
1317
  "retry-after"
@@ -1327,20 +1342,19 @@ class Zotero:
1327
1342
  headers = {"Zotero-Write-Token": token()}
1328
1343
  if last_modified is not None:
1329
1344
  headers["If-Unmodified-Since-Version"] = str(last_modified)
1330
- headers.update(self.default_headers())
1331
1345
  self._check_backoff()
1332
- req = requests.post(
1346
+ req = self.client.post(
1333
1347
  url=build_url(
1334
1348
  self.endpoint,
1335
1349
  "/{t}/{u}/collections".format(t=self.library_type, u=self.library_id),
1336
1350
  ),
1337
1351
  headers=headers,
1338
- data=json.dumps(payload),
1352
+ content=json.dumps(payload),
1339
1353
  )
1340
1354
  self.request = req
1341
1355
  try:
1342
1356
  req.raise_for_status()
1343
- except requests.exceptions.HTTPError as exc:
1357
+ except httpx.HTTPError as exc:
1344
1358
  error_handler(self, req, exc)
1345
1359
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1346
1360
  if backoff:
@@ -1359,9 +1373,8 @@ class Zotero:
1359
1373
  modified = last_modified
1360
1374
  key = payload["key"]
1361
1375
  headers = {"If-Unmodified-Since-Version": str(modified)}
1362
- headers.update(self.default_headers())
1363
1376
  headers.update({"Content-Type": "application/json"})
1364
- return requests.put(
1377
+ return self.client.put(
1365
1378
  url=build_url(
1366
1379
  self.endpoint,
1367
1380
  "/{t}/{u}/collections/{c}".format(
@@ -1369,7 +1382,7 @@ class Zotero:
1369
1382
  ),
1370
1383
  ),
1371
1384
  headers=headers,
1372
- data=json.dumps(payload),
1385
+ content=json.dumps(payload),
1373
1386
  )
1374
1387
 
1375
1388
  def attachment_simple(self, files, parentid=None):
@@ -1419,8 +1432,7 @@ class Zotero:
1419
1432
  modified = last_modified
1420
1433
  ident = payload["key"]
1421
1434
  headers = {"If-Unmodified-Since-Version": str(modified)}
1422
- headers.update(self.default_headers())
1423
- return requests.patch(
1435
+ return self.client.patch(
1424
1436
  url=build_url(
1425
1437
  self.endpoint,
1426
1438
  "/{t}/{u}/items/{id}".format(
@@ -1428,7 +1440,7 @@ class Zotero:
1428
1440
  ),
1429
1441
  ),
1430
1442
  headers=headers,
1431
- data=json.dumps(to_send),
1443
+ content=json.dumps(to_send),
1432
1444
  )
1433
1445
 
1434
1446
  def update_items(self, payload):
@@ -1437,24 +1449,21 @@ class Zotero:
1437
1449
  Accepts one argument, a list of dicts containing Item data
1438
1450
  """
1439
1451
  to_send = [self.check_items([p])[0] for p in payload]
1440
- headers = {}
1441
- headers.update(self.default_headers())
1442
1452
  # the API only accepts 50 items at a time, so we have to split
1443
1453
  # anything longer
1444
1454
  for chunk in chunks(to_send, 50):
1445
1455
  self._check_backoff()
1446
- req = requests.post(
1456
+ req = self.client.post(
1447
1457
  url=build_url(
1448
1458
  self.endpoint,
1449
1459
  "/{t}/{u}/items/".format(t=self.library_type, u=self.library_id),
1450
1460
  ),
1451
- headers=headers,
1452
- data=json.dumps(chunk),
1461
+ content=json.dumps(chunk),
1453
1462
  )
1454
1463
  self.request = req
1455
1464
  try:
1456
1465
  req.raise_for_status()
1457
- except requests.exceptions.HTTPError as exc:
1466
+ except httpx.HTTPError as exc:
1458
1467
  error_handler(self, req, exc)
1459
1468
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1460
1469
  if backoff:
@@ -1467,26 +1476,23 @@ class Zotero:
1467
1476
  Accepts one argument, a list of dicts containing Collection data
1468
1477
  """
1469
1478
  to_send = [self.check_items([p])[0] for p in payload]
1470
- headers = {}
1471
- headers.update(self.default_headers())
1472
1479
  # the API only accepts 50 items at a time, so we have to split
1473
1480
  # anything longer
1474
1481
  for chunk in chunks(to_send, 50):
1475
1482
  self._check_backoff()
1476
- req = requests.post(
1483
+ req = self.client.post(
1477
1484
  url=build_url(
1478
1485
  self.endpoint,
1479
1486
  "/{t}/{u}/collections/".format(
1480
1487
  t=self.library_type, u=self.library_id
1481
1488
  ),
1482
1489
  ),
1483
- headers=headers,
1484
- data=json.dumps(chunk),
1490
+ content=json.dumps(chunk),
1485
1491
  )
1486
1492
  self.request = req
1487
1493
  try:
1488
1494
  req.raise_for_status()
1489
- except requests.exceptions.HTTPError as exc:
1495
+ except httpx.HTTPError as exc:
1490
1496
  error_handler(self, req, exc)
1491
1497
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1492
1498
  if backoff:
@@ -1505,15 +1511,14 @@ class Zotero:
1505
1511
  # add the collection data from the item
1506
1512
  modified_collections = payload["data"]["collections"] + [collection]
1507
1513
  headers = {"If-Unmodified-Since-Version": str(modified)}
1508
- headers.update(self.default_headers())
1509
- return requests.patch(
1514
+ return self.client.patch(
1510
1515
  url=build_url(
1511
1516
  self.endpoint,
1512
1517
  "/{t}/{u}/items/{i}".format(
1513
1518
  t=self.library_type, u=self.library_id, i=ident
1514
1519
  ),
1515
1520
  ),
1516
- data=json.dumps({"collections": modified_collections}),
1521
+ content=json.dumps({"collections": modified_collections}),
1517
1522
  headers=headers,
1518
1523
  )
1519
1524
 
@@ -1531,15 +1536,14 @@ class Zotero:
1531
1536
  c for c in payload["data"]["collections"] if c != collection
1532
1537
  ]
1533
1538
  headers = {"If-Unmodified-Since-Version": str(modified)}
1534
- headers.update(self.default_headers())
1535
- return requests.patch(
1539
+ return self.client.patch(
1536
1540
  url=build_url(
1537
1541
  self.endpoint,
1538
1542
  "/{t}/{u}/items/{i}".format(
1539
1543
  t=self.library_type, u=self.library_id, i=ident
1540
1544
  ),
1541
1545
  ),
1542
- data=json.dumps({"collections": modified_collections}),
1546
+ content=json.dumps({"collections": modified_collections}),
1543
1547
  headers=headers,
1544
1548
  )
1545
1549
 
@@ -1558,8 +1562,7 @@ class Zotero:
1558
1562
  headers = {
1559
1563
  "If-Unmodified-Since-Version": self.request.headers["last-modified-version"]
1560
1564
  }
1561
- headers.update(self.default_headers())
1562
- return requests.delete(
1565
+ return self.client.delete(
1563
1566
  url=build_url(
1564
1567
  self.endpoint,
1565
1568
  "/{t}/{u}/tags".format(t=self.library_type, u=self.library_id),
@@ -1600,8 +1603,7 @@ class Zotero:
1600
1603
  ),
1601
1604
  )
1602
1605
  headers = {"If-Unmodified-Since-Version": str(modified)}
1603
- headers.update(self.default_headers())
1604
- return requests.delete(url=url, params=params, headers=headers)
1606
+ return self.client.delete(url=url, params=params, headers=headers)
1605
1607
 
1606
1608
  @backoff_check
1607
1609
  def delete_collection(self, payload, last_modified=None):
@@ -1635,8 +1637,7 @@ class Zotero:
1635
1637
  ),
1636
1638
  )
1637
1639
  headers = {"If-Unmodified-Since-Version": str(modified)}
1638
- headers.update(self.default_headers())
1639
- return requests.delete(url=url, params=params, headers=headers)
1640
+ return self.client.delete(url=url, params=params, headers=headers)
1640
1641
 
1641
1642
 
1642
1643
  def error_handler(zot, req, exc=None):
@@ -1655,13 +1656,7 @@ def error_handler(zot, req, exc=None):
1655
1656
 
1656
1657
  def err_msg(req):
1657
1658
  """Return a nicely-formatted error message"""
1658
- return "\nCode: %s\nURL: %s\nMethod: %s\nResponse: %s" % (
1659
- req.status_code,
1660
- # error.msg,
1661
- req.url,
1662
- req.request.method,
1663
- req.text,
1664
- )
1659
+ return f"\nCode: {req.status_code}\nURL: {str(req.url)}\nMethod: {req.request.method}\nResponse: {req.text}"
1665
1660
 
1666
1661
  if error_codes.get(req.status_code):
1667
1662
  # check to see whether its 429
@@ -1830,12 +1825,11 @@ class SavedSearch:
1830
1825
  for condition in conditions:
1831
1826
  if set(condition.keys()) != allowed_keys:
1832
1827
  raise ze.ParamNotPassed(
1833
- "Keys must be all of: %s" % ", ".join(self.searchkeys)
1828
+ f"Keys must be all of: {', '.join(self.searchkeys)}"
1834
1829
  )
1835
1830
  if condition.get("operator") not in operators_set:
1836
1831
  raise ze.ParamNotPassed(
1837
- "You have specified an unknown operator: %s"
1838
- % condition.get("operator")
1832
+ f"You have specified an unknown operator: {condition.get('operator')}"
1839
1833
  )
1840
1834
  # dict keys of allowed operators for the current condition
1841
1835
  permitted_operators = self.conditions_operators.get(
@@ -1847,12 +1841,7 @@ class SavedSearch:
1847
1841
  )
1848
1842
  if condition.get("operator") not in permitted_operators_list:
1849
1843
  raise ze.ParamNotPassed(
1850
- "You may not use the '%s' operator when selecting the '%s' condition. \nAllowed operators: %s"
1851
- % (
1852
- condition.get("operator"),
1853
- condition.get("condition"),
1854
- ", ".join(list(permitted_operators_list)),
1855
- )
1844
+ f"You may not use the '{condition.get('operator')}' operator when selecting the '{condition.get('condition')}' condition. \nAllowed operators: {', '.join(list(permitted_operators_list))}"
1856
1845
  )
1857
1846
 
1858
1847
 
@@ -1890,14 +1879,12 @@ class Zupload:
1890
1879
  pass
1891
1880
  except IOError:
1892
1881
  raise ze.FileDoesNotExist(
1893
- "The file at %s couldn't be opened or found."
1894
- % str(self.basedir.joinpath(templt["filename"]))
1882
+ f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
1895
1883
  )
1896
1884
  # no point in continuing if the file isn't a file
1897
1885
  else:
1898
1886
  raise ze.FileDoesNotExist(
1899
- "The file at %s couldn't be opened or found."
1900
- % str(self.basedir.joinpath(templt["filename"]))
1887
+ f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
1901
1888
  )
1902
1889
 
1903
1890
  def _create_prelim(self):
@@ -1914,26 +1901,25 @@ class Zupload:
1914
1901
  liblevel = "/{t}/{u}/items"
1915
1902
  # Create one or more new attachments
1916
1903
  headers = {"Zotero-Write-Token": token(), "Content-Type": "application/json"}
1917
- headers.update(self.zinstance.default_headers())
1918
1904
  # If we have a Parent ID, add it as a parentItem
1919
1905
  if self.parentid:
1920
1906
  for child in self.payload:
1921
1907
  child["parentItem"] = self.parentid
1922
1908
  to_send = json.dumps(self.payload)
1923
1909
  self.zinstance._check_backoff()
1924
- req = requests.post(
1910
+ req = self.client.post(
1925
1911
  url=build_url(
1926
1912
  self.zinstance.endpoint,
1927
1913
  liblevel.format(
1928
1914
  t=self.zinstance.library_type, u=self.zinstance.library_id
1929
1915
  ),
1930
1916
  ),
1931
- data=to_send,
1917
+ content=to_send,
1932
1918
  headers=headers,
1933
1919
  )
1934
1920
  try:
1935
1921
  req.raise_for_status()
1936
- except requests.exceptions.HTTPError as exc:
1922
+ except httpx.HTTPError as exc:
1937
1923
  error_handler(self.zinstance, req, exc)
1938
1924
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1939
1925
  if backoff:
@@ -1958,7 +1944,6 @@ class Zupload:
1958
1944
  else:
1959
1945
  # docs specify that for existing file we use this
1960
1946
  auth_headers["If-Match"] = md5
1961
- auth_headers.update(self.zinstance.default_headers())
1962
1947
  data = {
1963
1948
  "md5": digest.hexdigest(),
1964
1949
  "filename": os.path.basename(attachment),
@@ -1969,7 +1954,7 @@ class Zupload:
1969
1954
  "params": 1,
1970
1955
  }
1971
1956
  self.zinstance._check_backoff()
1972
- auth_req = requests.post(
1957
+ auth_req = self.zinstance.client.post(
1973
1958
  url=build_url(
1974
1959
  self.zinstance.endpoint,
1975
1960
  "/{t}/{u}/items/{i}/file".format(
@@ -1978,12 +1963,12 @@ class Zupload:
1978
1963
  i=reg_key,
1979
1964
  ),
1980
1965
  ),
1981
- data=data,
1966
+ content=data,
1982
1967
  headers=auth_headers,
1983
1968
  )
1984
1969
  try:
1985
1970
  auth_req.raise_for_status()
1986
- except requests.exceptions.HTTPError as exc:
1971
+ except httpx.HTTPError as exc:
1987
1972
  error_handler(self.zinstance, auth_req, exc)
1988
1973
  backoff = auth_req.headers.get("backoff") or auth_req.headers.get("retry-after")
1989
1974
  if backoff:
@@ -2006,16 +1991,16 @@ class Zupload:
2006
1991
  upload_pairs = tuple(upload_list)
2007
1992
  try:
2008
1993
  self.zinstance._check_backoff()
2009
- upload = requests.post(
1994
+ upload = self.client.post(
2010
1995
  url=authdata["url"],
2011
1996
  files=upload_pairs,
2012
- headers={"User-Agent": "Pyzotero/%s" % pz.__version__},
1997
+ headers={"User-Agent": f"Pyzotero/{pz.__version__}"},
2013
1998
  )
2014
- except requests.exceptions.ConnectionError:
1999
+ except httpx.ConnectionError:
2015
2000
  raise ze.UploadError("ConnectionError")
2016
2001
  try:
2017
2002
  upload.raise_for_status()
2018
- except requests.exceptions.HTTPError as exc:
2003
+ except httpx.HTTPError as exc:
2019
2004
  error_handler(self.zinstance, upload, exc)
2020
2005
  backoff = upload.headers.get("backoff") or upload.headers.get("retry-after")
2021
2006
  if backoff:
@@ -2031,10 +2016,9 @@ class Zupload:
2031
2016
  "Content-Type": "application/x-www-form-urlencoded",
2032
2017
  "If-None-Match": "*",
2033
2018
  }
2034
- reg_headers.update(self.zinstance.default_headers())
2035
2019
  reg_data = {"upload": authdata.get("uploadKey")}
2036
2020
  self.zinstance._check_backoff()
2037
- upload_reg = requests.post(
2021
+ upload_reg = self.zinstane.client.post(
2038
2022
  url=build_url(
2039
2023
  self.zinstance.endpoint,
2040
2024
  "/{t}/{u}/items/{i}/file".format(
@@ -2043,12 +2027,12 @@ class Zupload:
2043
2027
  i=reg_key,
2044
2028
  ),
2045
2029
  ),
2046
- data=reg_data,
2030
+ content=reg_data,
2047
2031
  headers=dict(reg_headers),
2048
2032
  )
2049
2033
  try:
2050
2034
  upload_reg.raise_for_status()
2051
- except requests.exceptions.HTTPError as exc:
2035
+ except httpx.HTTPError as exc:
2052
2036
  error_handler(self.zinstance, upload_reg, exc)
2053
2037
  backoff = upload_reg.headers.get("backoff") or upload_reg.headers.get(
2054
2038
  "retry-after"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyzotero
3
- Version: 1.6.0
3
+ Version: 1.6.2
4
4
  Summary: Python wrapper for the Zotero API
5
5
  Author-email: Stephan Hügel <urschrei@gmail.com>
6
6
  License: # Blue Oak Model License
@@ -61,12 +61,13 @@ License-File: LICENSE.md
61
61
  License-File: AUTHORS
62
62
  Requires-Dist: feedparser>=6.0.11
63
63
  Requires-Dist: pytz
64
- Requires-Dist: requests>=2.21.0
65
64
  Requires-Dist: bibtexparser
65
+ Requires-Dist: httpx>=0.28.1
66
66
  Provides-Extra: test
67
67
  Requires-Dist: pytest>=7.4.2; extra == "test"
68
68
  Requires-Dist: httpretty; extra == "test"
69
69
  Requires-Dist: python-dateutil; extra == "test"
70
+ Requires-Dist: ipython; extra == "test"
70
71
 
71
72
  [![Supported Python versions](https://img.shields.io/pypi/pyversions/Pyzotero.svg?style=flat)](https://pypi.python.org/pypi/Pyzotero/) [![Docs](https://readthedocs.org/projects/pyzotero/badge/?version=latest)](http://pyzotero.readthedocs.org/en/latest/?badge=latest) [![PyPI Version](https://img.shields.io/pypi/v/Pyzotero.svg)](https://pypi.python.org/pypi/Pyzotero) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyzotero/badges/version.svg)](https://anaconda.org/conda-forge/pyzotero) [![Downloads](https://pepy.tech/badge/pyzotero)](https://pepy.tech/project/pyzotero)
72
73
 
@@ -86,7 +87,7 @@ Then:
86
87
 
87
88
  ``` python
88
89
  from pyzotero import zotero
89
- zot = zotero.Zotero(library_id, library_type, api_key)
90
+ zot = zotero.Zotero(library_id, library_type, api_key) # local=True for read access to local Zotero
90
91
  items = zot.top(limit=5)
91
92
  # we've retrieved the latest five top-level items in our library
92
93
  # we can print each item's item type and ID
@@ -109,7 +110,7 @@ Example:
109
110
  ``` bash
110
111
  git clone git://github.com/urschrei/pyzotero.git
111
112
  cd pyzotero
112
- git checkout dev
113
+ git checkout main
113
114
  pip install .
114
115
  ```
115
116
 
@@ -119,7 +120,7 @@ Run `pytest .` from the top-level directory.
119
120
 
120
121
  ## Issues
121
122
 
122
- The latest commits can be found on the [dev branch][9], although new features are currently rare. If you encounter an error, please open an issue.
123
+ The latest commits can be found on the [main branch][9], although new features are currently rare. If you encounter an error, please open an issue.
123
124
 
124
125
  ## Pull Requests
125
126
 
@@ -145,14 +146,14 @@ A sample citation (APA 6th edition) might look like:
145
146
 
146
147
  # License
147
148
 
148
- Pyzotero is licensed under the [Blue Oak Model Licence 1.0.0][8]. See [LICENCE.md](LICENCE.md) for details.
149
+ Pyzotero is licensed under the [Blue Oak Model Licence 1.0.0][8]. See [LICENSE.md](LICENSE.md) for details.
149
150
 
150
151
  [1]: https://www.zotero.org/support/dev/web_api/v3/start
151
152
  [2]: https://www.zotero.org/settings/keys/new
152
153
  [3]: http://pyzotero.readthedocs.org/en/latest/
153
154
  [7]: https://nose2.readthedocs.io/en/latest/
154
155
  [8]: https://opensource.org/license/blue-oak-model-license
155
- [9]: https://github.com/urschrei/pyzotero/tree/dev
156
+ [9]: https://github.com/urschrei/pyzotero/tree/main
156
157
  [10]: http://www.pip-installer.org/en/latest/index.html
157
158
  † This isn't strictly true: you only need an API key for personal libraries and non-public group libraries.
158
159
 
@@ -0,0 +1,10 @@
1
+ _version.py,sha256=ay9A4GSmtr3NioHirRgXvWfXtjwRjzXIO_WPuFobCoI,411
2
+ pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
3
+ pyzotero/zotero.py,sha256=jnfqgwbn2nMxUnUNG98dm--AlE6CaLyJCuOizXOvJz4,75376
4
+ pyzotero/zotero_errors.py,sha256=UPhAmf2K05cnoeIl2wjufWQedepg7vBKb-ShU0TdlL4,2582
5
+ pyzotero-1.6.2.dist-info/AUTHORS,sha256=ZMicxg7lRScOYbxzMPznlzMbmrFIUIHwg-NvljEMbRQ,110
6
+ pyzotero-1.6.2.dist-info/LICENSE.md,sha256=bhy1CPMj1zWffD9YifFmSeBzPylsrhb1qP8OCEx5Etw,1550
7
+ pyzotero-1.6.2.dist-info/METADATA,sha256=aFEP8H1c0A7W75pQO_qBh6HNNcqDmnlRZ47llubffwc,7289
8
+ pyzotero-1.6.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
9
+ pyzotero-1.6.2.dist-info/top_level.txt,sha256=BOPNkPk5VtNDCy_li7Xftx6k0zG8STGxh-KgckcxLEw,18
10
+ pyzotero-1.6.2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- _version.py,sha256=Ry70pc5l-IBhT9gahlkNwZPp4g0CzVEWqsat9H-UASY,411
2
- pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
3
- pyzotero/zotero.py,sha256=qij4_ihYYzZI-18_CPZD2sexqs0Iya1E61dcaSZpmZo,76201
4
- pyzotero/zotero_errors.py,sha256=UPhAmf2K05cnoeIl2wjufWQedepg7vBKb-ShU0TdlL4,2582
5
- pyzotero-1.6.0.dist-info/AUTHORS,sha256=ZMicxg7lRScOYbxzMPznlzMbmrFIUIHwg-NvljEMbRQ,110
6
- pyzotero-1.6.0.dist-info/LICENSE.md,sha256=bhy1CPMj1zWffD9YifFmSeBzPylsrhb1qP8OCEx5Etw,1550
7
- pyzotero-1.6.0.dist-info/METADATA,sha256=vSYfI-tzsTaMafBXfjZFvPqXMrFHjW2EHB9uAepQHeE,7204
8
- pyzotero-1.6.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
9
- pyzotero-1.6.0.dist-info/top_level.txt,sha256=BOPNkPk5VtNDCy_li7Xftx6k0zG8STGxh-KgckcxLEw,18
10
- pyzotero-1.6.0.dist-info/RECORD,,