pyzotero 1.6.1__py3-none-any.whl → 1.6.3__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.1'
16
- __version_tuple__ = version_tuple = (1, 6, 1)
15
+ __version__ = version = '1.6.3'
16
+ __version_tuple__ = version_tuple = (1, 6, 3)
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
 
@@ -62,6 +62,23 @@ def build_url(base_url, path, args_dict=None):
62
62
  return urlunparse(url_parts)
63
63
 
64
64
 
65
+ def merge_params(url, params):
66
+ """This function strips query parameters, extracting them into a dict, then merging it with
67
+ the "params" dict, returning the truncated url and merged query params dict"""
68
+ parsed = urlparse(url)
69
+ # Extract query parameters from URL
70
+ incoming = parse_qs(parsed.query)
71
+ incoming = {k: v[0] for k, v in incoming.items()}
72
+
73
+ # Create new params dict by merging
74
+ merged = {**incoming, **params}
75
+
76
+ # Get base URL by zeroing out the query component
77
+ base_url = urlunparse(parsed._replace(query=""))
78
+
79
+ return base_url, merged
80
+
81
+
65
82
  def token():
66
83
  """Return a unique 32-char write-token"""
67
84
  return str(uuid.uuid4().hex)
@@ -97,11 +114,14 @@ def tcache(func):
97
114
  "GET",
98
115
  build_url(self.endpoint, query_string),
99
116
  params=params,
100
- ).prepare()
117
+ )
118
+ with httpx.Client() as client:
119
+ response = client.send(r)
120
+
101
121
  # now split up the URL
102
- result = urlparse(r.url)
122
+ result = urlparse(str(response.url))
103
123
  # construct cache key
104
- cachekey = result.path + "_" + result.query
124
+ cachekey = f"{result.path}_{result.query}"
105
125
  if self.templates.get(cachekey) and not self._updated(
106
126
  query_string, self.templates[cachekey], cachekey
107
127
  ):
@@ -132,7 +152,7 @@ def backoff_check(func):
132
152
  resp = func(self, *args, **kwargs)
133
153
  try:
134
154
  resp.raise_for_status()
135
- except requests.exceptions.HTTPError as exc:
155
+ except httpx.HTTPError as exc:
136
156
  error_handler(self, resp, exc)
137
157
  self.request = resp
138
158
  backoff = resp.headers.get("backoff") or resp.headers.get("retry-after")
@@ -166,8 +186,8 @@ def retrieve(func):
166
186
  self.links = self._extract_links()
167
187
  # determine content and format, based on url params
168
188
  content = (
169
- self.content.search(self.request.url)
170
- and self.content.search(self.request.url).group(0)
189
+ self.content.search(str(self.request.url))
190
+ and self.content.search(str(self.request.url)).group(0)
171
191
  or "bib"
172
192
  )
173
193
  # JSON by default
@@ -223,7 +243,7 @@ def retrieve(func):
223
243
  file = retrieved.content
224
244
  return file
225
245
  # check to see whether it's tag data
226
- if "tags" in self.request.url:
246
+ if "tags" in str(self.request.url):
227
247
  self.tag_data = False
228
248
  return self._tags_data(retrieved.json())
229
249
  if fmt == "atom":
@@ -277,6 +297,7 @@ class Zotero:
277
297
  locale="en-US",
278
298
  local=False,
279
299
  ):
300
+ self.client = None
280
301
  """Store Zotero credentials"""
281
302
  if not local:
282
303
  self.endpoint = "https://api.zotero.org"
@@ -300,6 +321,7 @@ class Zotero:
300
321
  self.tag_data = False
301
322
  self.request = None
302
323
  self.snapshot = False
324
+ self.client = httpx.Client(headers=self.default_headers())
303
325
  # these aren't valid item fields, so never send them to the server
304
326
  self.temp_keys = set(["key", "etag", "group_id", "updated"])
305
327
  # determine which processor to use for the parsed content
@@ -331,6 +353,11 @@ class Zotero:
331
353
  self.backoff = False
332
354
  self.backoff_duration = 0.0
333
355
 
356
+ def __del__(self):
357
+ # this isn't guaranteed to run, but that's OK
358
+ if c := self.client:
359
+ c.close()
360
+
334
361
  def _check_for_component(self, url, component):
335
362
  """Check a url path query fragment for a specific query parameter"""
336
363
  if parse_qs(url).get(component):
@@ -381,11 +408,11 @@ class Zotero:
381
408
  It's always OK to include these headers
382
409
  """
383
410
  _headers = {
384
- "User-Agent": "Pyzotero/%s" % pz.__version__,
385
- "Zotero-API-Version": "%s" % __api_version__,
411
+ "User-Agent": f"Pyzotero/{pz.__version__}",
412
+ "Zotero-API-Version": f"{__api_version__}",
386
413
  }
387
414
  if self.api_key:
388
- _headers["Authorization"] = "Bearer %s" % self.api_key
415
+ _headers["Authorization"] = f"Bearer {self.api_key}"
389
416
  return _headers
390
417
 
391
418
  def _cache(self, response, key):
@@ -442,13 +469,26 @@ class Zotero:
442
469
  params["locale"] = self.locale
443
470
  else:
444
471
  params = {"locale": self.locale}
445
- self.request = requests.get(
446
- url=full_url, headers=self.default_headers(), params=params
472
+ # we now have to merge self.url_params (default params, and those supplied by the user)
473
+ if not params:
474
+ params = {}
475
+ if not self.url_params:
476
+ self.url_params = {}
477
+ merged_params = params | self.url_params
478
+ # our incoming url might be from the "links" dict, in which case it will contain url parameters.
479
+ # Unfortunately, httpx doesn't like to merge query paramaters in the url string and passed params
480
+ # so we strip the url params, combining them with our existing url_params
481
+ final_url, final_params = merge_params(full_url, merged_params)
482
+ self.request = self.client.get(
483
+ url=final_url,
484
+ params=final_params,
485
+ headers=self.default_headers(),
486
+ timeout=timeout,
447
487
  )
448
488
  self.request.encoding = "utf-8"
449
489
  try:
450
490
  self.request.raise_for_status()
451
- except requests.exceptions.HTTPError as exc:
491
+ except httpx.HTTPError as exc:
452
492
  error_handler(self, self.request, exc)
453
493
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
454
494
  "retry-after"
@@ -465,7 +505,7 @@ class Zotero:
465
505
  try:
466
506
  for key, value in self.request.links.items():
467
507
  parsed = urlparse(value["url"])
468
- fragment = "{path}?{query}".format(path=parsed[2], query=parsed[4])
508
+ fragment = f"{parsed[2]}?{parsed[4]}"
469
509
  extracted[key] = fragment
470
510
  # add a 'self' link
471
511
  parsed = list(urlparse(self.self_link))
@@ -510,13 +550,12 @@ class Zotero:
510
550
  "%a, %d %b %Y %H:%M:%S %Z"
511
551
  )
512
552
  }
513
- headers.update(self.default_headers())
514
553
  # perform the request, and check whether the response returns 304
515
554
  self._check_backoff()
516
- req = requests.get(query, headers=headers)
555
+ req = self.client.get(query, headers=headers)
517
556
  try:
518
557
  req.raise_for_status()
519
- except requests.exceptions.HTTPError as exc:
558
+ except httpx.HTTPError as exc:
520
559
  error_handler(self, req, exc)
521
560
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
522
561
  "retry-after"
@@ -550,7 +589,7 @@ class Zotero:
550
589
  # bib format can't have a limit
551
590
  if params.get("format") == "bib":
552
591
  del params["limit"]
553
- self.url_params = urlencode(params, doseq=True)
592
+ self.url_params = params
554
593
 
555
594
  def _build_query(self, query_string, no_params=False):
556
595
  """
@@ -560,12 +599,11 @@ class Zotero:
560
599
  try:
561
600
  query = quote(query_string.format(u=self.library_id, t=self.library_type))
562
601
  except KeyError as err:
563
- raise ze.ParamNotPassed("There's a request parameter missing: %s" % err)
602
+ raise ze.ParamNotPassed(f"There's a request parameter missing: {err}")
564
603
  # Add the URL parameters and the user key, if necessary
565
604
  if no_params is False:
566
605
  if not self.url_params:
567
606
  self.add_parameters()
568
- query = "%s?%s" % (query, self.url_params)
569
607
  return query
570
608
 
571
609
  @retrieve
@@ -638,9 +676,9 @@ class Zotero:
638
676
  For text documents, 'indexedChars' and 'totalChars' OR
639
677
  For PDFs, 'indexedPages' and 'totalPages'.
640
678
  """
641
- headers = self.default_headers()
679
+ headers = {}
642
680
  headers.update({"Content-Type": "application/json"})
643
- return requests.put(
681
+ return self.client.put(
644
682
  url=build_url(
645
683
  self.endpoint,
646
684
  "/{t}/{u}/items/{k}/fulltext".format(
@@ -648,7 +686,7 @@ class Zotero:
648
686
  ),
649
687
  ),
650
688
  headers=headers,
651
- data=json.dumps(payload),
689
+ content=json.dumps(payload),
652
690
  )
653
691
 
654
692
  def new_fulltext(self, since):
@@ -656,15 +694,18 @@ class Zotero:
656
694
  Retrieve list of full-text content items and versions which are newer
657
695
  than <since>
658
696
  """
659
- query_string = "/{t}/{u}/fulltext?since={version}".format(
660
- t=self.library_type, u=self.library_id, version=since
697
+ query_string = "/{t}/{u}/fulltext".format(
698
+ t=self.library_type, u=self.library_id
661
699
  )
662
- headers = self.default_headers()
700
+ headers = {}
701
+ params = {"since": since}
663
702
  self._check_backoff()
664
- resp = requests.get(build_url(self.endpoint, query_string), headers=headers)
703
+ resp = self.client.get(
704
+ build_url(self.endpoint, query_string), params=params, headers=headers
705
+ )
665
706
  try:
666
707
  resp.raise_for_status()
667
- except requests.exceptions.HTTPError as exc:
708
+ except httpx.HTTPError as exc:
668
709
  error_handler(self, resp, exc)
669
710
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
670
711
  "retry-after"
@@ -879,7 +920,6 @@ class Zotero:
879
920
  """Return the result of the call to the URL in the 'Next' link"""
880
921
  if n := self.links.get("next"):
881
922
  newurl = self._striplocal(n)
882
- print(newurl)
883
923
  return newurl
884
924
  return
885
925
 
@@ -984,17 +1024,17 @@ class Zotero:
984
1024
  def item_template(self, itemtype, linkmode=None):
985
1025
  """Get a template for a new item"""
986
1026
  # if we have a template and it hasn't been updated since we stored it
987
- template_name = "{}_{}_{}".format(*["item_template", itemtype, linkmode or ""])
988
- query_string = "/items/new?itemType={i}".format(i=itemtype)
1027
+ template_name = f"item_template_{itemtype}_{linkmode or ''}"
1028
+ params = {"itemType": itemtype}
1029
+ # Set linkMode parameter for API request if itemtype is attachment
1030
+ if itemtype == "attachment":
1031
+ params["linkMode"] = linkmode
1032
+ self.add_parameters(**params)
1033
+ query_string = "/items/new"
989
1034
  if self.templates.get(template_name) and not self._updated(
990
1035
  query_string, self.templates[template_name], template_name
991
1036
  ):
992
1037
  return copy.deepcopy(self.templates[template_name]["tmplt"])
993
-
994
- # Set linkMode parameter for API request if itemtype is attachment
995
- if itemtype == "attachment":
996
- query_string = "{}&linkMode={}".format(query_string, linkmode)
997
-
998
1038
  # otherwise perform a normal request and cache the response
999
1039
  retrieved = self._retrieve_data(query_string)
1000
1040
  return self._cache(retrieved, template_name)
@@ -1050,20 +1090,19 @@ class Zotero:
1050
1090
  self.savedsearch._validate(conditions)
1051
1091
  payload = [{"name": name, "conditions": conditions}]
1052
1092
  headers = {"Zotero-Write-Token": token()}
1053
- headers.update(self.default_headers())
1054
1093
  self._check_backoff()
1055
- req = requests.post(
1094
+ req = self.client.post(
1056
1095
  url=build_url(
1057
1096
  self.endpoint,
1058
1097
  "/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
1059
1098
  ),
1060
1099
  headers=headers,
1061
- data=json.dumps(payload),
1100
+ content=json.dumps(payload),
1062
1101
  )
1063
1102
  self.request = req
1064
1103
  try:
1065
1104
  req.raise_for_status()
1066
- except requests.exceptions.HTTPError as exc:
1105
+ except httpx.HTTPError as exc:
1067
1106
  error_handler(self, req, exc)
1068
1107
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
1069
1108
  "retry-after"
@@ -1078,9 +1117,8 @@ class Zotero:
1078
1117
  unique search keys
1079
1118
  """
1080
1119
  headers = {"Zotero-Write-Token": token()}
1081
- headers.update(self.default_headers())
1082
1120
  self._check_backoff()
1083
- req = requests.delete(
1121
+ req = self.client.delete(
1084
1122
  url=build_url(
1085
1123
  self.endpoint,
1086
1124
  "/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
@@ -1091,7 +1129,7 @@ class Zotero:
1091
1129
  self.request = req
1092
1130
  try:
1093
1131
  req.raise_for_status()
1094
- except requests.exceptions.HTTPError as exc:
1132
+ except httpx.HTTPError as exc:
1095
1133
  error_handler(self, req, exc)
1096
1134
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
1097
1135
  "retry-after"
@@ -1117,7 +1155,7 @@ class Zotero:
1117
1155
  except AssertionError:
1118
1156
  item["data"]["tags"] = list()
1119
1157
  for tag in tags:
1120
- item["data"]["tags"].append({"tag": "%s" % tag})
1158
+ item["data"]["tags"].append({"tag": f"{tag}"})
1121
1159
  # make sure everything's OK
1122
1160
  assert self.check_items([item])
1123
1161
  return self.update_item(item)
@@ -1134,9 +1172,11 @@ class Zotero:
1134
1172
  "GET",
1135
1173
  build_url(self.endpoint, query_string),
1136
1174
  params=params,
1137
- ).prepare()
1175
+ )
1176
+ with httpx.Client() as client:
1177
+ response = client.send(r)
1138
1178
  # now split up the URL
1139
- result = urlparse(r.url)
1179
+ result = urlparse(str(response.url))
1140
1180
  # construct cache key
1141
1181
  cachekey = result.path + "_" + result.query
1142
1182
  if self.templates.get(cachekey) and not self._updated(
@@ -1189,8 +1229,7 @@ class Zotero:
1189
1229
  difference = to_check.difference(template)
1190
1230
  if difference:
1191
1231
  raise ze.InvalidItemFields(
1192
- "Invalid keys present in item %s: %s"
1193
- % (pos + 1, " ".join(i for i in difference))
1232
+ f"Invalid keys present in item {pos + 1}: {' '.join(i for i in difference)}"
1194
1233
  )
1195
1234
  return items
1196
1235
 
@@ -1254,20 +1293,19 @@ class Zotero:
1254
1293
  if last_modified is not None:
1255
1294
  headers["If-Unmodified-Since-Version"] = str(last_modified)
1256
1295
  to_send = json.dumps([i for i in self._cleanup(*payload, allow=("key"))])
1257
- headers.update(self.default_headers())
1258
1296
  self._check_backoff()
1259
- req = requests.post(
1297
+ req = self.client.post(
1260
1298
  url=build_url(
1261
1299
  self.endpoint,
1262
1300
  "/{t}/{u}/items".format(t=self.library_type, u=self.library_id),
1263
1301
  ),
1264
- data=to_send,
1302
+ content=to_send,
1265
1303
  headers=dict(headers),
1266
1304
  )
1267
1305
  self.request = req
1268
1306
  try:
1269
1307
  req.raise_for_status()
1270
- except requests.exceptions.HTTPError as exc:
1308
+ except httpx.HTTPError as exc:
1271
1309
  error_handler(self, req, exc)
1272
1310
  resp = req.json()
1273
1311
  backoff = self.request.headers.get("backoff") or self.request.headers.get(
@@ -1282,24 +1320,23 @@ class Zotero:
1282
1320
  uheaders = {
1283
1321
  "If-Unmodified-Since-Version": req.headers["last-modified-version"]
1284
1322
  }
1285
- uheaders.update(self.default_headers())
1286
1323
  for value in resp["success"].values():
1287
1324
  payload = json.dumps({"parentItem": parentid})
1288
1325
  self._check_backoff()
1289
- presp = requests.patch(
1326
+ presp = self.client.patch(
1290
1327
  url=build_url(
1291
1328
  self.endpoint,
1292
1329
  "/{t}/{u}/items/{v}".format(
1293
1330
  t=self.library_type, u=self.library_id, v=value
1294
1331
  ),
1295
1332
  ),
1296
- data=payload,
1333
+ content=payload,
1297
1334
  headers=dict(uheaders),
1298
1335
  )
1299
1336
  self.request = presp
1300
1337
  try:
1301
1338
  presp.raise_for_status()
1302
- except requests.exceptions.HTTPError as exc:
1339
+ except httpx.HTTPError as exc:
1303
1340
  error_handler(self, presp, exc)
1304
1341
  backoff = presp.headers.get("backoff") or presp.headers.get(
1305
1342
  "retry-after"
@@ -1330,20 +1367,19 @@ class Zotero:
1330
1367
  headers = {"Zotero-Write-Token": token()}
1331
1368
  if last_modified is not None:
1332
1369
  headers["If-Unmodified-Since-Version"] = str(last_modified)
1333
- headers.update(self.default_headers())
1334
1370
  self._check_backoff()
1335
- req = requests.post(
1371
+ req = self.client.post(
1336
1372
  url=build_url(
1337
1373
  self.endpoint,
1338
1374
  "/{t}/{u}/collections".format(t=self.library_type, u=self.library_id),
1339
1375
  ),
1340
1376
  headers=headers,
1341
- data=json.dumps(payload),
1377
+ content=json.dumps(payload),
1342
1378
  )
1343
1379
  self.request = req
1344
1380
  try:
1345
1381
  req.raise_for_status()
1346
- except requests.exceptions.HTTPError as exc:
1382
+ except httpx.HTTPError as exc:
1347
1383
  error_handler(self, req, exc)
1348
1384
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1349
1385
  if backoff:
@@ -1362,9 +1398,8 @@ class Zotero:
1362
1398
  modified = last_modified
1363
1399
  key = payload["key"]
1364
1400
  headers = {"If-Unmodified-Since-Version": str(modified)}
1365
- headers.update(self.default_headers())
1366
1401
  headers.update({"Content-Type": "application/json"})
1367
- return requests.put(
1402
+ return self.client.put(
1368
1403
  url=build_url(
1369
1404
  self.endpoint,
1370
1405
  "/{t}/{u}/collections/{c}".format(
@@ -1372,7 +1407,7 @@ class Zotero:
1372
1407
  ),
1373
1408
  ),
1374
1409
  headers=headers,
1375
- data=json.dumps(payload),
1410
+ content=json.dumps(payload),
1376
1411
  )
1377
1412
 
1378
1413
  def attachment_simple(self, files, parentid=None):
@@ -1422,8 +1457,7 @@ class Zotero:
1422
1457
  modified = last_modified
1423
1458
  ident = payload["key"]
1424
1459
  headers = {"If-Unmodified-Since-Version": str(modified)}
1425
- headers.update(self.default_headers())
1426
- return requests.patch(
1460
+ return self.client.patch(
1427
1461
  url=build_url(
1428
1462
  self.endpoint,
1429
1463
  "/{t}/{u}/items/{id}".format(
@@ -1431,7 +1465,7 @@ class Zotero:
1431
1465
  ),
1432
1466
  ),
1433
1467
  headers=headers,
1434
- data=json.dumps(to_send),
1468
+ content=json.dumps(to_send),
1435
1469
  )
1436
1470
 
1437
1471
  def update_items(self, payload):
@@ -1440,24 +1474,21 @@ class Zotero:
1440
1474
  Accepts one argument, a list of dicts containing Item data
1441
1475
  """
1442
1476
  to_send = [self.check_items([p])[0] for p in payload]
1443
- headers = {}
1444
- headers.update(self.default_headers())
1445
1477
  # the API only accepts 50 items at a time, so we have to split
1446
1478
  # anything longer
1447
1479
  for chunk in chunks(to_send, 50):
1448
1480
  self._check_backoff()
1449
- req = requests.post(
1481
+ req = self.client.post(
1450
1482
  url=build_url(
1451
1483
  self.endpoint,
1452
1484
  "/{t}/{u}/items/".format(t=self.library_type, u=self.library_id),
1453
1485
  ),
1454
- headers=headers,
1455
- data=json.dumps(chunk),
1486
+ content=json.dumps(chunk),
1456
1487
  )
1457
1488
  self.request = req
1458
1489
  try:
1459
1490
  req.raise_for_status()
1460
- except requests.exceptions.HTTPError as exc:
1491
+ except httpx.HTTPError as exc:
1461
1492
  error_handler(self, req, exc)
1462
1493
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1463
1494
  if backoff:
@@ -1470,26 +1501,23 @@ class Zotero:
1470
1501
  Accepts one argument, a list of dicts containing Collection data
1471
1502
  """
1472
1503
  to_send = [self.check_items([p])[0] for p in payload]
1473
- headers = {}
1474
- headers.update(self.default_headers())
1475
1504
  # the API only accepts 50 items at a time, so we have to split
1476
1505
  # anything longer
1477
1506
  for chunk in chunks(to_send, 50):
1478
1507
  self._check_backoff()
1479
- req = requests.post(
1508
+ req = self.client.post(
1480
1509
  url=build_url(
1481
1510
  self.endpoint,
1482
1511
  "/{t}/{u}/collections/".format(
1483
1512
  t=self.library_type, u=self.library_id
1484
1513
  ),
1485
1514
  ),
1486
- headers=headers,
1487
- data=json.dumps(chunk),
1515
+ content=json.dumps(chunk),
1488
1516
  )
1489
1517
  self.request = req
1490
1518
  try:
1491
1519
  req.raise_for_status()
1492
- except requests.exceptions.HTTPError as exc:
1520
+ except httpx.HTTPError as exc:
1493
1521
  error_handler(self, req, exc)
1494
1522
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1495
1523
  if backoff:
@@ -1508,15 +1536,14 @@ class Zotero:
1508
1536
  # add the collection data from the item
1509
1537
  modified_collections = payload["data"]["collections"] + [collection]
1510
1538
  headers = {"If-Unmodified-Since-Version": str(modified)}
1511
- headers.update(self.default_headers())
1512
- return requests.patch(
1539
+ return self.client.patch(
1513
1540
  url=build_url(
1514
1541
  self.endpoint,
1515
1542
  "/{t}/{u}/items/{i}".format(
1516
1543
  t=self.library_type, u=self.library_id, i=ident
1517
1544
  ),
1518
1545
  ),
1519
- data=json.dumps({"collections": modified_collections}),
1546
+ content=json.dumps({"collections": modified_collections}),
1520
1547
  headers=headers,
1521
1548
  )
1522
1549
 
@@ -1534,15 +1561,14 @@ class Zotero:
1534
1561
  c for c in payload["data"]["collections"] if c != collection
1535
1562
  ]
1536
1563
  headers = {"If-Unmodified-Since-Version": str(modified)}
1537
- headers.update(self.default_headers())
1538
- return requests.patch(
1564
+ return self.client.patch(
1539
1565
  url=build_url(
1540
1566
  self.endpoint,
1541
1567
  "/{t}/{u}/items/{i}".format(
1542
1568
  t=self.library_type, u=self.library_id, i=ident
1543
1569
  ),
1544
1570
  ),
1545
- data=json.dumps({"collections": modified_collections}),
1571
+ content=json.dumps({"collections": modified_collections}),
1546
1572
  headers=headers,
1547
1573
  )
1548
1574
 
@@ -1561,8 +1587,7 @@ class Zotero:
1561
1587
  headers = {
1562
1588
  "If-Unmodified-Since-Version": self.request.headers["last-modified-version"]
1563
1589
  }
1564
- headers.update(self.default_headers())
1565
- return requests.delete(
1590
+ return self.client.delete(
1566
1591
  url=build_url(
1567
1592
  self.endpoint,
1568
1593
  "/{t}/{u}/tags".format(t=self.library_type, u=self.library_id),
@@ -1603,8 +1628,7 @@ class Zotero:
1603
1628
  ),
1604
1629
  )
1605
1630
  headers = {"If-Unmodified-Since-Version": str(modified)}
1606
- headers.update(self.default_headers())
1607
- return requests.delete(url=url, params=params, headers=headers)
1631
+ return self.client.delete(url=url, params=params, headers=headers)
1608
1632
 
1609
1633
  @backoff_check
1610
1634
  def delete_collection(self, payload, last_modified=None):
@@ -1638,8 +1662,7 @@ class Zotero:
1638
1662
  ),
1639
1663
  )
1640
1664
  headers = {"If-Unmodified-Since-Version": str(modified)}
1641
- headers.update(self.default_headers())
1642
- return requests.delete(url=url, params=params, headers=headers)
1665
+ return self.client.delete(url=url, params=params, headers=headers)
1643
1666
 
1644
1667
 
1645
1668
  def error_handler(zot, req, exc=None):
@@ -1658,13 +1681,7 @@ def error_handler(zot, req, exc=None):
1658
1681
 
1659
1682
  def err_msg(req):
1660
1683
  """Return a nicely-formatted error message"""
1661
- return "\nCode: %s\nURL: %s\nMethod: %s\nResponse: %s" % (
1662
- req.status_code,
1663
- # error.msg,
1664
- req.url,
1665
- req.request.method,
1666
- req.text,
1667
- )
1684
+ return f"\nCode: {req.status_code}\nURL: {str(req.url)}\nMethod: {req.request.method}\nResponse: {req.text}"
1668
1685
 
1669
1686
  if error_codes.get(req.status_code):
1670
1687
  # check to see whether its 429
@@ -1833,12 +1850,11 @@ class SavedSearch:
1833
1850
  for condition in conditions:
1834
1851
  if set(condition.keys()) != allowed_keys:
1835
1852
  raise ze.ParamNotPassed(
1836
- "Keys must be all of: %s" % ", ".join(self.searchkeys)
1853
+ f"Keys must be all of: {', '.join(self.searchkeys)}"
1837
1854
  )
1838
1855
  if condition.get("operator") not in operators_set:
1839
1856
  raise ze.ParamNotPassed(
1840
- "You have specified an unknown operator: %s"
1841
- % condition.get("operator")
1857
+ f"You have specified an unknown operator: {condition.get('operator')}"
1842
1858
  )
1843
1859
  # dict keys of allowed operators for the current condition
1844
1860
  permitted_operators = self.conditions_operators.get(
@@ -1850,12 +1866,7 @@ class SavedSearch:
1850
1866
  )
1851
1867
  if condition.get("operator") not in permitted_operators_list:
1852
1868
  raise ze.ParamNotPassed(
1853
- "You may not use the '%s' operator when selecting the '%s' condition. \nAllowed operators: %s"
1854
- % (
1855
- condition.get("operator"),
1856
- condition.get("condition"),
1857
- ", ".join(list(permitted_operators_list)),
1858
- )
1869
+ f"You may not use the '{condition.get('operator')}' operator when selecting the '{condition.get('condition')}' condition. \nAllowed operators: {', '.join(list(permitted_operators_list))}"
1859
1870
  )
1860
1871
 
1861
1872
 
@@ -1893,14 +1904,12 @@ class Zupload:
1893
1904
  pass
1894
1905
  except IOError:
1895
1906
  raise ze.FileDoesNotExist(
1896
- "The file at %s couldn't be opened or found."
1897
- % str(self.basedir.joinpath(templt["filename"]))
1907
+ f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
1898
1908
  )
1899
1909
  # no point in continuing if the file isn't a file
1900
1910
  else:
1901
1911
  raise ze.FileDoesNotExist(
1902
- "The file at %s couldn't be opened or found."
1903
- % str(self.basedir.joinpath(templt["filename"]))
1912
+ f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
1904
1913
  )
1905
1914
 
1906
1915
  def _create_prelim(self):
@@ -1917,26 +1926,25 @@ class Zupload:
1917
1926
  liblevel = "/{t}/{u}/items"
1918
1927
  # Create one or more new attachments
1919
1928
  headers = {"Zotero-Write-Token": token(), "Content-Type": "application/json"}
1920
- headers.update(self.zinstance.default_headers())
1921
1929
  # If we have a Parent ID, add it as a parentItem
1922
1930
  if self.parentid:
1923
1931
  for child in self.payload:
1924
1932
  child["parentItem"] = self.parentid
1925
1933
  to_send = json.dumps(self.payload)
1926
1934
  self.zinstance._check_backoff()
1927
- req = requests.post(
1935
+ req = self.client.post(
1928
1936
  url=build_url(
1929
1937
  self.zinstance.endpoint,
1930
1938
  liblevel.format(
1931
1939
  t=self.zinstance.library_type, u=self.zinstance.library_id
1932
1940
  ),
1933
1941
  ),
1934
- data=to_send,
1942
+ content=to_send,
1935
1943
  headers=headers,
1936
1944
  )
1937
1945
  try:
1938
1946
  req.raise_for_status()
1939
- except requests.exceptions.HTTPError as exc:
1947
+ except httpx.HTTPError as exc:
1940
1948
  error_handler(self.zinstance, req, exc)
1941
1949
  backoff = req.headers.get("backoff") or req.headers.get("retry-after")
1942
1950
  if backoff:
@@ -1961,7 +1969,6 @@ class Zupload:
1961
1969
  else:
1962
1970
  # docs specify that for existing file we use this
1963
1971
  auth_headers["If-Match"] = md5
1964
- auth_headers.update(self.zinstance.default_headers())
1965
1972
  data = {
1966
1973
  "md5": digest.hexdigest(),
1967
1974
  "filename": os.path.basename(attachment),
@@ -1972,7 +1979,7 @@ class Zupload:
1972
1979
  "params": 1,
1973
1980
  }
1974
1981
  self.zinstance._check_backoff()
1975
- auth_req = requests.post(
1982
+ auth_req = self.zinstance.client.post(
1976
1983
  url=build_url(
1977
1984
  self.zinstance.endpoint,
1978
1985
  "/{t}/{u}/items/{i}/file".format(
@@ -1981,12 +1988,12 @@ class Zupload:
1981
1988
  i=reg_key,
1982
1989
  ),
1983
1990
  ),
1984
- data=data,
1991
+ content=data,
1985
1992
  headers=auth_headers,
1986
1993
  )
1987
1994
  try:
1988
1995
  auth_req.raise_for_status()
1989
- except requests.exceptions.HTTPError as exc:
1996
+ except httpx.HTTPError as exc:
1990
1997
  error_handler(self.zinstance, auth_req, exc)
1991
1998
  backoff = auth_req.headers.get("backoff") or auth_req.headers.get("retry-after")
1992
1999
  if backoff:
@@ -2009,16 +2016,16 @@ class Zupload:
2009
2016
  upload_pairs = tuple(upload_list)
2010
2017
  try:
2011
2018
  self.zinstance._check_backoff()
2012
- upload = requests.post(
2019
+ upload = self.client.post(
2013
2020
  url=authdata["url"],
2014
2021
  files=upload_pairs,
2015
- headers={"User-Agent": "Pyzotero/%s" % pz.__version__},
2022
+ headers={"User-Agent": f"Pyzotero/{pz.__version__}"},
2016
2023
  )
2017
- except requests.exceptions.ConnectionError:
2024
+ except httpx.ConnectionError:
2018
2025
  raise ze.UploadError("ConnectionError")
2019
2026
  try:
2020
2027
  upload.raise_for_status()
2021
- except requests.exceptions.HTTPError as exc:
2028
+ except httpx.HTTPError as exc:
2022
2029
  error_handler(self.zinstance, upload, exc)
2023
2030
  backoff = upload.headers.get("backoff") or upload.headers.get("retry-after")
2024
2031
  if backoff:
@@ -2034,10 +2041,9 @@ class Zupload:
2034
2041
  "Content-Type": "application/x-www-form-urlencoded",
2035
2042
  "If-None-Match": "*",
2036
2043
  }
2037
- reg_headers.update(self.zinstance.default_headers())
2038
2044
  reg_data = {"upload": authdata.get("uploadKey")}
2039
2045
  self.zinstance._check_backoff()
2040
- upload_reg = requests.post(
2046
+ upload_reg = self.zinstane.client.post(
2041
2047
  url=build_url(
2042
2048
  self.zinstance.endpoint,
2043
2049
  "/{t}/{u}/items/{i}/file".format(
@@ -2046,12 +2052,12 @@ class Zupload:
2046
2052
  i=reg_key,
2047
2053
  ),
2048
2054
  ),
2049
- data=reg_data,
2055
+ content=reg_data,
2050
2056
  headers=dict(reg_headers),
2051
2057
  )
2052
2058
  try:
2053
2059
  upload_reg.raise_for_status()
2054
- except requests.exceptions.HTTPError as exc:
2060
+ except httpx.HTTPError as exc:
2055
2061
  error_handler(self.zinstance, upload_reg, exc)
2056
2062
  backoff = upload_reg.headers.get("backoff") or upload_reg.headers.get(
2057
2063
  "retry-after"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyzotero
3
- Version: 1.6.1
3
+ Version: 1.6.3
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
@@ -43,11 +43,11 @@ Project-URL: Repository, https://github.com/urschrei/pyzotero
43
43
  Project-URL: Tracker, https://github.com/urschrei/pyzotero/issues
44
44
  Keywords: Zotero,DH
45
45
  Classifier: Programming Language :: Python
46
- Classifier: Programming Language :: Python :: 3.8
47
46
  Classifier: Programming Language :: Python :: 3.9
48
47
  Classifier: Programming Language :: Python :: 3.10
49
48
  Classifier: Programming Language :: Python :: 3.11
50
49
  Classifier: Programming Language :: Python :: 3.12
50
+ Classifier: Programming Language :: Python :: 3.13
51
51
  Classifier: Development Status :: 5 - Production/Stable
52
52
  Classifier: Intended Audience :: Developers
53
53
  Classifier: Intended Audience :: Science/Research
@@ -55,18 +55,19 @@ Classifier: Intended Audience :: Education
55
55
  Classifier: License :: OSI Approved :: Blue Oak Model License (BlueOak-1.0.0)
56
56
  Classifier: Operating System :: OS Independent
57
57
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
58
- Requires-Python: >=3.8
58
+ Requires-Python: >=3.9
59
59
  Description-Content-Type: text/markdown
60
60
  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=mQ_8947spH9F9E4bJgRMJ3LZK_sGORi1ak9UVDzTrr8,411
2
+ pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
3
+ pyzotero/zotero.py,sha256=xR7pbpvNjhLteNWfUUe56WmVEy73I0YsqcoXnW2LWhc,76408
4
+ pyzotero/zotero_errors.py,sha256=UPhAmf2K05cnoeIl2wjufWQedepg7vBKb-ShU0TdlL4,2582
5
+ pyzotero-1.6.3.dist-info/AUTHORS,sha256=ZMicxg7lRScOYbxzMPznlzMbmrFIUIHwg-NvljEMbRQ,110
6
+ pyzotero-1.6.3.dist-info/LICENSE.md,sha256=bhy1CPMj1zWffD9YifFmSeBzPylsrhb1qP8OCEx5Etw,1550
7
+ pyzotero-1.6.3.dist-info/METADATA,sha256=_sSykeMd0j3ndkriUjdoKsCain3uIxMhAqSY9vhnlm8,7290
8
+ pyzotero-1.6.3.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
9
+ pyzotero-1.6.3.dist-info/top_level.txt,sha256=BOPNkPk5VtNDCy_li7Xftx6k0zG8STGxh-KgckcxLEw,18
10
+ pyzotero-1.6.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.7.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- _version.py,sha256=edFVVa8HpVPfLqL2y6CKtViSqJREfmXxInA-HCy-134,411
2
- pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
3
- pyzotero/zotero.py,sha256=Qk3vuJxv4rbD_Q0sg_8nc8QXVAboJ9_eZWrLuJLkW_4,76313
4
- pyzotero/zotero_errors.py,sha256=UPhAmf2K05cnoeIl2wjufWQedepg7vBKb-ShU0TdlL4,2582
5
- pyzotero-1.6.1.dist-info/AUTHORS,sha256=ZMicxg7lRScOYbxzMPznlzMbmrFIUIHwg-NvljEMbRQ,110
6
- pyzotero-1.6.1.dist-info/LICENSE.md,sha256=bhy1CPMj1zWffD9YifFmSeBzPylsrhb1qP8OCEx5Etw,1550
7
- pyzotero-1.6.1.dist-info/METADATA,sha256=TCxweUVtZbjMDFDrFtZMDzKCFm4j8VMNkwzbJ2kJUZ0,7204
8
- pyzotero-1.6.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
9
- pyzotero-1.6.1.dist-info/top_level.txt,sha256=BOPNkPk5VtNDCy_li7Xftx6k0zG8STGxh-KgckcxLEw,18
10
- pyzotero-1.6.1.dist-info/RECORD,,