pyzotero 1.6.1__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 +2 -2
- pyzotero/zotero.py +109 -128
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.2.dist-info}/METADATA +8 -7
- pyzotero-1.6.2.dist-info/RECORD +10 -0
- pyzotero-1.6.1.dist-info/RECORD +0 -10
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.2.dist-info}/AUTHORS +0 -0
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.2.dist-info}/LICENSE.md +0 -0
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.2.dist-info}/WHEEL +0 -0
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.2.dist-info}/top_level.txt +0 -0
_version.py
CHANGED
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
|
|
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
|
-
)
|
|
100
|
+
)
|
|
101
|
+
with httpx.Client() as client:
|
|
102
|
+
response = client.send(r)
|
|
103
|
+
|
|
101
104
|
# now split up the URL
|
|
102
|
-
result = urlparse(
|
|
105
|
+
result = urlparse(str(response.url))
|
|
103
106
|
# construct cache key
|
|
104
|
-
cachekey = result.path
|
|
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
|
|
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):
|
|
@@ -381,11 +391,11 @@ class Zotero:
|
|
|
381
391
|
It's always OK to include these headers
|
|
382
392
|
"""
|
|
383
393
|
_headers = {
|
|
384
|
-
"User-Agent": "Pyzotero
|
|
385
|
-
"Zotero-API-Version": "
|
|
394
|
+
"User-Agent": f"Pyzotero/{pz.__version__}",
|
|
395
|
+
"Zotero-API-Version": f"{__api_version__}",
|
|
386
396
|
}
|
|
387
397
|
if self.api_key:
|
|
388
|
-
_headers["Authorization"] = "Bearer
|
|
398
|
+
_headers["Authorization"] = f"Bearer {self.api_key}"
|
|
389
399
|
return _headers
|
|
390
400
|
|
|
391
401
|
def _cache(self, response, key):
|
|
@@ -442,13 +452,17 @@ class Zotero:
|
|
|
442
452
|
params["locale"] = self.locale
|
|
443
453
|
else:
|
|
444
454
|
params = {"locale": self.locale}
|
|
445
|
-
self.
|
|
446
|
-
|
|
447
|
-
|
|
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)
|
|
448
462
|
self.request.encoding = "utf-8"
|
|
449
463
|
try:
|
|
450
464
|
self.request.raise_for_status()
|
|
451
|
-
except
|
|
465
|
+
except httpx.HTTPError as exc:
|
|
452
466
|
error_handler(self, self.request, exc)
|
|
453
467
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
454
468
|
"retry-after"
|
|
@@ -465,7 +479,7 @@ class Zotero:
|
|
|
465
479
|
try:
|
|
466
480
|
for key, value in self.request.links.items():
|
|
467
481
|
parsed = urlparse(value["url"])
|
|
468
|
-
fragment = "{
|
|
482
|
+
fragment = f"{parsed[2]}?{parsed[4]}"
|
|
469
483
|
extracted[key] = fragment
|
|
470
484
|
# add a 'self' link
|
|
471
485
|
parsed = list(urlparse(self.self_link))
|
|
@@ -510,13 +524,12 @@ class Zotero:
|
|
|
510
524
|
"%a, %d %b %Y %H:%M:%S %Z"
|
|
511
525
|
)
|
|
512
526
|
}
|
|
513
|
-
headers.update(self.default_headers())
|
|
514
527
|
# perform the request, and check whether the response returns 304
|
|
515
528
|
self._check_backoff()
|
|
516
|
-
req =
|
|
529
|
+
req = self.client.get(query, headers=headers)
|
|
517
530
|
try:
|
|
518
531
|
req.raise_for_status()
|
|
519
|
-
except
|
|
532
|
+
except httpx.HTTPError as exc:
|
|
520
533
|
error_handler(self, req, exc)
|
|
521
534
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
522
535
|
"retry-after"
|
|
@@ -550,7 +563,7 @@ class Zotero:
|
|
|
550
563
|
# bib format can't have a limit
|
|
551
564
|
if params.get("format") == "bib":
|
|
552
565
|
del params["limit"]
|
|
553
|
-
self.url_params =
|
|
566
|
+
self.url_params = params
|
|
554
567
|
|
|
555
568
|
def _build_query(self, query_string, no_params=False):
|
|
556
569
|
"""
|
|
@@ -560,12 +573,11 @@ class Zotero:
|
|
|
560
573
|
try:
|
|
561
574
|
query = quote(query_string.format(u=self.library_id, t=self.library_type))
|
|
562
575
|
except KeyError as err:
|
|
563
|
-
raise ze.ParamNotPassed("There's a request parameter missing:
|
|
576
|
+
raise ze.ParamNotPassed(f"There's a request parameter missing: {err}")
|
|
564
577
|
# Add the URL parameters and the user key, if necessary
|
|
565
578
|
if no_params is False:
|
|
566
579
|
if not self.url_params:
|
|
567
580
|
self.add_parameters()
|
|
568
|
-
query = "%s?%s" % (query, self.url_params)
|
|
569
581
|
return query
|
|
570
582
|
|
|
571
583
|
@retrieve
|
|
@@ -638,9 +650,9 @@ class Zotero:
|
|
|
638
650
|
For text documents, 'indexedChars' and 'totalChars' OR
|
|
639
651
|
For PDFs, 'indexedPages' and 'totalPages'.
|
|
640
652
|
"""
|
|
641
|
-
headers =
|
|
653
|
+
headers = {}
|
|
642
654
|
headers.update({"Content-Type": "application/json"})
|
|
643
|
-
return
|
|
655
|
+
return self.client.put(
|
|
644
656
|
url=build_url(
|
|
645
657
|
self.endpoint,
|
|
646
658
|
"/{t}/{u}/items/{k}/fulltext".format(
|
|
@@ -648,7 +660,7 @@ class Zotero:
|
|
|
648
660
|
),
|
|
649
661
|
),
|
|
650
662
|
headers=headers,
|
|
651
|
-
|
|
663
|
+
content=json.dumps(payload),
|
|
652
664
|
)
|
|
653
665
|
|
|
654
666
|
def new_fulltext(self, since):
|
|
@@ -656,15 +668,18 @@ class Zotero:
|
|
|
656
668
|
Retrieve list of full-text content items and versions which are newer
|
|
657
669
|
than <since>
|
|
658
670
|
"""
|
|
659
|
-
query_string = "/{t}/{u}/fulltext
|
|
660
|
-
t=self.library_type, u=self.library_id
|
|
671
|
+
query_string = "/{t}/{u}/fulltext".format(
|
|
672
|
+
t=self.library_type, u=self.library_id
|
|
661
673
|
)
|
|
662
|
-
headers =
|
|
674
|
+
headers = {}
|
|
675
|
+
params = {"since": since}
|
|
663
676
|
self._check_backoff()
|
|
664
|
-
resp =
|
|
677
|
+
resp = self.client.get(
|
|
678
|
+
build_url(self.endpoint, query_string), params=params, headers=headers
|
|
679
|
+
)
|
|
665
680
|
try:
|
|
666
681
|
resp.raise_for_status()
|
|
667
|
-
except
|
|
682
|
+
except httpx.HTTPError as exc:
|
|
668
683
|
error_handler(self, resp, exc)
|
|
669
684
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
670
685
|
"retry-after"
|
|
@@ -984,17 +999,17 @@ class Zotero:
|
|
|
984
999
|
def item_template(self, itemtype, linkmode=None):
|
|
985
1000
|
"""Get a template for a new item"""
|
|
986
1001
|
# if we have a template and it hasn't been updated since we stored it
|
|
987
|
-
template_name = "{}_{
|
|
988
|
-
|
|
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"
|
|
989
1009
|
if self.templates.get(template_name) and not self._updated(
|
|
990
1010
|
query_string, self.templates[template_name], template_name
|
|
991
1011
|
):
|
|
992
1012
|
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
1013
|
# otherwise perform a normal request and cache the response
|
|
999
1014
|
retrieved = self._retrieve_data(query_string)
|
|
1000
1015
|
return self._cache(retrieved, template_name)
|
|
@@ -1050,20 +1065,19 @@ class Zotero:
|
|
|
1050
1065
|
self.savedsearch._validate(conditions)
|
|
1051
1066
|
payload = [{"name": name, "conditions": conditions}]
|
|
1052
1067
|
headers = {"Zotero-Write-Token": token()}
|
|
1053
|
-
headers.update(self.default_headers())
|
|
1054
1068
|
self._check_backoff()
|
|
1055
|
-
req =
|
|
1069
|
+
req = self.client.post(
|
|
1056
1070
|
url=build_url(
|
|
1057
1071
|
self.endpoint,
|
|
1058
1072
|
"/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
|
|
1059
1073
|
),
|
|
1060
1074
|
headers=headers,
|
|
1061
|
-
|
|
1075
|
+
content=json.dumps(payload),
|
|
1062
1076
|
)
|
|
1063
1077
|
self.request = req
|
|
1064
1078
|
try:
|
|
1065
1079
|
req.raise_for_status()
|
|
1066
|
-
except
|
|
1080
|
+
except httpx.HTTPError as exc:
|
|
1067
1081
|
error_handler(self, req, exc)
|
|
1068
1082
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
1069
1083
|
"retry-after"
|
|
@@ -1078,9 +1092,8 @@ class Zotero:
|
|
|
1078
1092
|
unique search keys
|
|
1079
1093
|
"""
|
|
1080
1094
|
headers = {"Zotero-Write-Token": token()}
|
|
1081
|
-
headers.update(self.default_headers())
|
|
1082
1095
|
self._check_backoff()
|
|
1083
|
-
req =
|
|
1096
|
+
req = self.client.delete(
|
|
1084
1097
|
url=build_url(
|
|
1085
1098
|
self.endpoint,
|
|
1086
1099
|
"/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
|
|
@@ -1091,7 +1104,7 @@ class Zotero:
|
|
|
1091
1104
|
self.request = req
|
|
1092
1105
|
try:
|
|
1093
1106
|
req.raise_for_status()
|
|
1094
|
-
except
|
|
1107
|
+
except httpx.HTTPError as exc:
|
|
1095
1108
|
error_handler(self, req, exc)
|
|
1096
1109
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
1097
1110
|
"retry-after"
|
|
@@ -1117,7 +1130,7 @@ class Zotero:
|
|
|
1117
1130
|
except AssertionError:
|
|
1118
1131
|
item["data"]["tags"] = list()
|
|
1119
1132
|
for tag in tags:
|
|
1120
|
-
item["data"]["tags"].append({"tag": "
|
|
1133
|
+
item["data"]["tags"].append({"tag": f"{tag}"})
|
|
1121
1134
|
# make sure everything's OK
|
|
1122
1135
|
assert self.check_items([item])
|
|
1123
1136
|
return self.update_item(item)
|
|
@@ -1134,9 +1147,11 @@ class Zotero:
|
|
|
1134
1147
|
"GET",
|
|
1135
1148
|
build_url(self.endpoint, query_string),
|
|
1136
1149
|
params=params,
|
|
1137
|
-
)
|
|
1150
|
+
)
|
|
1151
|
+
with httpx.Client() as client:
|
|
1152
|
+
response = client.send(r)
|
|
1138
1153
|
# now split up the URL
|
|
1139
|
-
result = urlparse(
|
|
1154
|
+
result = urlparse(str(response.url))
|
|
1140
1155
|
# construct cache key
|
|
1141
1156
|
cachekey = result.path + "_" + result.query
|
|
1142
1157
|
if self.templates.get(cachekey) and not self._updated(
|
|
@@ -1189,8 +1204,7 @@ class Zotero:
|
|
|
1189
1204
|
difference = to_check.difference(template)
|
|
1190
1205
|
if difference:
|
|
1191
1206
|
raise ze.InvalidItemFields(
|
|
1192
|
-
"Invalid keys present in item
|
|
1193
|
-
% (pos + 1, " ".join(i for i in difference))
|
|
1207
|
+
f"Invalid keys present in item {pos + 1}: {' '.join(i for i in difference)}"
|
|
1194
1208
|
)
|
|
1195
1209
|
return items
|
|
1196
1210
|
|
|
@@ -1254,20 +1268,19 @@ class Zotero:
|
|
|
1254
1268
|
if last_modified is not None:
|
|
1255
1269
|
headers["If-Unmodified-Since-Version"] = str(last_modified)
|
|
1256
1270
|
to_send = json.dumps([i for i in self._cleanup(*payload, allow=("key"))])
|
|
1257
|
-
headers.update(self.default_headers())
|
|
1258
1271
|
self._check_backoff()
|
|
1259
|
-
req =
|
|
1272
|
+
req = self.client.post(
|
|
1260
1273
|
url=build_url(
|
|
1261
1274
|
self.endpoint,
|
|
1262
1275
|
"/{t}/{u}/items".format(t=self.library_type, u=self.library_id),
|
|
1263
1276
|
),
|
|
1264
|
-
|
|
1277
|
+
content=to_send,
|
|
1265
1278
|
headers=dict(headers),
|
|
1266
1279
|
)
|
|
1267
1280
|
self.request = req
|
|
1268
1281
|
try:
|
|
1269
1282
|
req.raise_for_status()
|
|
1270
|
-
except
|
|
1283
|
+
except httpx.HTTPError as exc:
|
|
1271
1284
|
error_handler(self, req, exc)
|
|
1272
1285
|
resp = req.json()
|
|
1273
1286
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
@@ -1282,24 +1295,23 @@ class Zotero:
|
|
|
1282
1295
|
uheaders = {
|
|
1283
1296
|
"If-Unmodified-Since-Version": req.headers["last-modified-version"]
|
|
1284
1297
|
}
|
|
1285
|
-
uheaders.update(self.default_headers())
|
|
1286
1298
|
for value in resp["success"].values():
|
|
1287
1299
|
payload = json.dumps({"parentItem": parentid})
|
|
1288
1300
|
self._check_backoff()
|
|
1289
|
-
presp =
|
|
1301
|
+
presp = self.client.patch(
|
|
1290
1302
|
url=build_url(
|
|
1291
1303
|
self.endpoint,
|
|
1292
1304
|
"/{t}/{u}/items/{v}".format(
|
|
1293
1305
|
t=self.library_type, u=self.library_id, v=value
|
|
1294
1306
|
),
|
|
1295
1307
|
),
|
|
1296
|
-
|
|
1308
|
+
content=payload,
|
|
1297
1309
|
headers=dict(uheaders),
|
|
1298
1310
|
)
|
|
1299
1311
|
self.request = presp
|
|
1300
1312
|
try:
|
|
1301
1313
|
presp.raise_for_status()
|
|
1302
|
-
except
|
|
1314
|
+
except httpx.HTTPError as exc:
|
|
1303
1315
|
error_handler(self, presp, exc)
|
|
1304
1316
|
backoff = presp.headers.get("backoff") or presp.headers.get(
|
|
1305
1317
|
"retry-after"
|
|
@@ -1330,20 +1342,19 @@ class Zotero:
|
|
|
1330
1342
|
headers = {"Zotero-Write-Token": token()}
|
|
1331
1343
|
if last_modified is not None:
|
|
1332
1344
|
headers["If-Unmodified-Since-Version"] = str(last_modified)
|
|
1333
|
-
headers.update(self.default_headers())
|
|
1334
1345
|
self._check_backoff()
|
|
1335
|
-
req =
|
|
1346
|
+
req = self.client.post(
|
|
1336
1347
|
url=build_url(
|
|
1337
1348
|
self.endpoint,
|
|
1338
1349
|
"/{t}/{u}/collections".format(t=self.library_type, u=self.library_id),
|
|
1339
1350
|
),
|
|
1340
1351
|
headers=headers,
|
|
1341
|
-
|
|
1352
|
+
content=json.dumps(payload),
|
|
1342
1353
|
)
|
|
1343
1354
|
self.request = req
|
|
1344
1355
|
try:
|
|
1345
1356
|
req.raise_for_status()
|
|
1346
|
-
except
|
|
1357
|
+
except httpx.HTTPError as exc:
|
|
1347
1358
|
error_handler(self, req, exc)
|
|
1348
1359
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1349
1360
|
if backoff:
|
|
@@ -1362,9 +1373,8 @@ class Zotero:
|
|
|
1362
1373
|
modified = last_modified
|
|
1363
1374
|
key = payload["key"]
|
|
1364
1375
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1365
|
-
headers.update(self.default_headers())
|
|
1366
1376
|
headers.update({"Content-Type": "application/json"})
|
|
1367
|
-
return
|
|
1377
|
+
return self.client.put(
|
|
1368
1378
|
url=build_url(
|
|
1369
1379
|
self.endpoint,
|
|
1370
1380
|
"/{t}/{u}/collections/{c}".format(
|
|
@@ -1372,7 +1382,7 @@ class Zotero:
|
|
|
1372
1382
|
),
|
|
1373
1383
|
),
|
|
1374
1384
|
headers=headers,
|
|
1375
|
-
|
|
1385
|
+
content=json.dumps(payload),
|
|
1376
1386
|
)
|
|
1377
1387
|
|
|
1378
1388
|
def attachment_simple(self, files, parentid=None):
|
|
@@ -1422,8 +1432,7 @@ class Zotero:
|
|
|
1422
1432
|
modified = last_modified
|
|
1423
1433
|
ident = payload["key"]
|
|
1424
1434
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1425
|
-
|
|
1426
|
-
return requests.patch(
|
|
1435
|
+
return self.client.patch(
|
|
1427
1436
|
url=build_url(
|
|
1428
1437
|
self.endpoint,
|
|
1429
1438
|
"/{t}/{u}/items/{id}".format(
|
|
@@ -1431,7 +1440,7 @@ class Zotero:
|
|
|
1431
1440
|
),
|
|
1432
1441
|
),
|
|
1433
1442
|
headers=headers,
|
|
1434
|
-
|
|
1443
|
+
content=json.dumps(to_send),
|
|
1435
1444
|
)
|
|
1436
1445
|
|
|
1437
1446
|
def update_items(self, payload):
|
|
@@ -1440,24 +1449,21 @@ class Zotero:
|
|
|
1440
1449
|
Accepts one argument, a list of dicts containing Item data
|
|
1441
1450
|
"""
|
|
1442
1451
|
to_send = [self.check_items([p])[0] for p in payload]
|
|
1443
|
-
headers = {}
|
|
1444
|
-
headers.update(self.default_headers())
|
|
1445
1452
|
# the API only accepts 50 items at a time, so we have to split
|
|
1446
1453
|
# anything longer
|
|
1447
1454
|
for chunk in chunks(to_send, 50):
|
|
1448
1455
|
self._check_backoff()
|
|
1449
|
-
req =
|
|
1456
|
+
req = self.client.post(
|
|
1450
1457
|
url=build_url(
|
|
1451
1458
|
self.endpoint,
|
|
1452
1459
|
"/{t}/{u}/items/".format(t=self.library_type, u=self.library_id),
|
|
1453
1460
|
),
|
|
1454
|
-
|
|
1455
|
-
data=json.dumps(chunk),
|
|
1461
|
+
content=json.dumps(chunk),
|
|
1456
1462
|
)
|
|
1457
1463
|
self.request = req
|
|
1458
1464
|
try:
|
|
1459
1465
|
req.raise_for_status()
|
|
1460
|
-
except
|
|
1466
|
+
except httpx.HTTPError as exc:
|
|
1461
1467
|
error_handler(self, req, exc)
|
|
1462
1468
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1463
1469
|
if backoff:
|
|
@@ -1470,26 +1476,23 @@ class Zotero:
|
|
|
1470
1476
|
Accepts one argument, a list of dicts containing Collection data
|
|
1471
1477
|
"""
|
|
1472
1478
|
to_send = [self.check_items([p])[0] for p in payload]
|
|
1473
|
-
headers = {}
|
|
1474
|
-
headers.update(self.default_headers())
|
|
1475
1479
|
# the API only accepts 50 items at a time, so we have to split
|
|
1476
1480
|
# anything longer
|
|
1477
1481
|
for chunk in chunks(to_send, 50):
|
|
1478
1482
|
self._check_backoff()
|
|
1479
|
-
req =
|
|
1483
|
+
req = self.client.post(
|
|
1480
1484
|
url=build_url(
|
|
1481
1485
|
self.endpoint,
|
|
1482
1486
|
"/{t}/{u}/collections/".format(
|
|
1483
1487
|
t=self.library_type, u=self.library_id
|
|
1484
1488
|
),
|
|
1485
1489
|
),
|
|
1486
|
-
|
|
1487
|
-
data=json.dumps(chunk),
|
|
1490
|
+
content=json.dumps(chunk),
|
|
1488
1491
|
)
|
|
1489
1492
|
self.request = req
|
|
1490
1493
|
try:
|
|
1491
1494
|
req.raise_for_status()
|
|
1492
|
-
except
|
|
1495
|
+
except httpx.HTTPError as exc:
|
|
1493
1496
|
error_handler(self, req, exc)
|
|
1494
1497
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1495
1498
|
if backoff:
|
|
@@ -1508,15 +1511,14 @@ class Zotero:
|
|
|
1508
1511
|
# add the collection data from the item
|
|
1509
1512
|
modified_collections = payload["data"]["collections"] + [collection]
|
|
1510
1513
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1511
|
-
|
|
1512
|
-
return requests.patch(
|
|
1514
|
+
return self.client.patch(
|
|
1513
1515
|
url=build_url(
|
|
1514
1516
|
self.endpoint,
|
|
1515
1517
|
"/{t}/{u}/items/{i}".format(
|
|
1516
1518
|
t=self.library_type, u=self.library_id, i=ident
|
|
1517
1519
|
),
|
|
1518
1520
|
),
|
|
1519
|
-
|
|
1521
|
+
content=json.dumps({"collections": modified_collections}),
|
|
1520
1522
|
headers=headers,
|
|
1521
1523
|
)
|
|
1522
1524
|
|
|
@@ -1534,15 +1536,14 @@ class Zotero:
|
|
|
1534
1536
|
c for c in payload["data"]["collections"] if c != collection
|
|
1535
1537
|
]
|
|
1536
1538
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1537
|
-
|
|
1538
|
-
return requests.patch(
|
|
1539
|
+
return self.client.patch(
|
|
1539
1540
|
url=build_url(
|
|
1540
1541
|
self.endpoint,
|
|
1541
1542
|
"/{t}/{u}/items/{i}".format(
|
|
1542
1543
|
t=self.library_type, u=self.library_id, i=ident
|
|
1543
1544
|
),
|
|
1544
1545
|
),
|
|
1545
|
-
|
|
1546
|
+
content=json.dumps({"collections": modified_collections}),
|
|
1546
1547
|
headers=headers,
|
|
1547
1548
|
)
|
|
1548
1549
|
|
|
@@ -1561,8 +1562,7 @@ class Zotero:
|
|
|
1561
1562
|
headers = {
|
|
1562
1563
|
"If-Unmodified-Since-Version": self.request.headers["last-modified-version"]
|
|
1563
1564
|
}
|
|
1564
|
-
|
|
1565
|
-
return requests.delete(
|
|
1565
|
+
return self.client.delete(
|
|
1566
1566
|
url=build_url(
|
|
1567
1567
|
self.endpoint,
|
|
1568
1568
|
"/{t}/{u}/tags".format(t=self.library_type, u=self.library_id),
|
|
@@ -1603,8 +1603,7 @@ class Zotero:
|
|
|
1603
1603
|
),
|
|
1604
1604
|
)
|
|
1605
1605
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1606
|
-
|
|
1607
|
-
return requests.delete(url=url, params=params, headers=headers)
|
|
1606
|
+
return self.client.delete(url=url, params=params, headers=headers)
|
|
1608
1607
|
|
|
1609
1608
|
@backoff_check
|
|
1610
1609
|
def delete_collection(self, payload, last_modified=None):
|
|
@@ -1638,8 +1637,7 @@ class Zotero:
|
|
|
1638
1637
|
),
|
|
1639
1638
|
)
|
|
1640
1639
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1641
|
-
|
|
1642
|
-
return requests.delete(url=url, params=params, headers=headers)
|
|
1640
|
+
return self.client.delete(url=url, params=params, headers=headers)
|
|
1643
1641
|
|
|
1644
1642
|
|
|
1645
1643
|
def error_handler(zot, req, exc=None):
|
|
@@ -1658,13 +1656,7 @@ def error_handler(zot, req, exc=None):
|
|
|
1658
1656
|
|
|
1659
1657
|
def err_msg(req):
|
|
1660
1658
|
"""Return a nicely-formatted error message"""
|
|
1661
|
-
return "\nCode:
|
|
1662
|
-
req.status_code,
|
|
1663
|
-
# error.msg,
|
|
1664
|
-
req.url,
|
|
1665
|
-
req.request.method,
|
|
1666
|
-
req.text,
|
|
1667
|
-
)
|
|
1659
|
+
return f"\nCode: {req.status_code}\nURL: {str(req.url)}\nMethod: {req.request.method}\nResponse: {req.text}"
|
|
1668
1660
|
|
|
1669
1661
|
if error_codes.get(req.status_code):
|
|
1670
1662
|
# check to see whether its 429
|
|
@@ -1833,12 +1825,11 @@ class SavedSearch:
|
|
|
1833
1825
|
for condition in conditions:
|
|
1834
1826
|
if set(condition.keys()) != allowed_keys:
|
|
1835
1827
|
raise ze.ParamNotPassed(
|
|
1836
|
-
"Keys must be all of:
|
|
1828
|
+
f"Keys must be all of: {', '.join(self.searchkeys)}"
|
|
1837
1829
|
)
|
|
1838
1830
|
if condition.get("operator") not in operators_set:
|
|
1839
1831
|
raise ze.ParamNotPassed(
|
|
1840
|
-
"You have specified an unknown operator:
|
|
1841
|
-
% condition.get("operator")
|
|
1832
|
+
f"You have specified an unknown operator: {condition.get('operator')}"
|
|
1842
1833
|
)
|
|
1843
1834
|
# dict keys of allowed operators for the current condition
|
|
1844
1835
|
permitted_operators = self.conditions_operators.get(
|
|
@@ -1850,12 +1841,7 @@ class SavedSearch:
|
|
|
1850
1841
|
)
|
|
1851
1842
|
if condition.get("operator") not in permitted_operators_list:
|
|
1852
1843
|
raise ze.ParamNotPassed(
|
|
1853
|
-
"You may not use the '
|
|
1854
|
-
% (
|
|
1855
|
-
condition.get("operator"),
|
|
1856
|
-
condition.get("condition"),
|
|
1857
|
-
", ".join(list(permitted_operators_list)),
|
|
1858
|
-
)
|
|
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))}"
|
|
1859
1845
|
)
|
|
1860
1846
|
|
|
1861
1847
|
|
|
@@ -1893,14 +1879,12 @@ class Zupload:
|
|
|
1893
1879
|
pass
|
|
1894
1880
|
except IOError:
|
|
1895
1881
|
raise ze.FileDoesNotExist(
|
|
1896
|
-
"The file at
|
|
1897
|
-
% str(self.basedir.joinpath(templt["filename"]))
|
|
1882
|
+
f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
|
|
1898
1883
|
)
|
|
1899
1884
|
# no point in continuing if the file isn't a file
|
|
1900
1885
|
else:
|
|
1901
1886
|
raise ze.FileDoesNotExist(
|
|
1902
|
-
"The file at
|
|
1903
|
-
% str(self.basedir.joinpath(templt["filename"]))
|
|
1887
|
+
f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
|
|
1904
1888
|
)
|
|
1905
1889
|
|
|
1906
1890
|
def _create_prelim(self):
|
|
@@ -1917,26 +1901,25 @@ class Zupload:
|
|
|
1917
1901
|
liblevel = "/{t}/{u}/items"
|
|
1918
1902
|
# Create one or more new attachments
|
|
1919
1903
|
headers = {"Zotero-Write-Token": token(), "Content-Type": "application/json"}
|
|
1920
|
-
headers.update(self.zinstance.default_headers())
|
|
1921
1904
|
# If we have a Parent ID, add it as a parentItem
|
|
1922
1905
|
if self.parentid:
|
|
1923
1906
|
for child in self.payload:
|
|
1924
1907
|
child["parentItem"] = self.parentid
|
|
1925
1908
|
to_send = json.dumps(self.payload)
|
|
1926
1909
|
self.zinstance._check_backoff()
|
|
1927
|
-
req =
|
|
1910
|
+
req = self.client.post(
|
|
1928
1911
|
url=build_url(
|
|
1929
1912
|
self.zinstance.endpoint,
|
|
1930
1913
|
liblevel.format(
|
|
1931
1914
|
t=self.zinstance.library_type, u=self.zinstance.library_id
|
|
1932
1915
|
),
|
|
1933
1916
|
),
|
|
1934
|
-
|
|
1917
|
+
content=to_send,
|
|
1935
1918
|
headers=headers,
|
|
1936
1919
|
)
|
|
1937
1920
|
try:
|
|
1938
1921
|
req.raise_for_status()
|
|
1939
|
-
except
|
|
1922
|
+
except httpx.HTTPError as exc:
|
|
1940
1923
|
error_handler(self.zinstance, req, exc)
|
|
1941
1924
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1942
1925
|
if backoff:
|
|
@@ -1961,7 +1944,6 @@ class Zupload:
|
|
|
1961
1944
|
else:
|
|
1962
1945
|
# docs specify that for existing file we use this
|
|
1963
1946
|
auth_headers["If-Match"] = md5
|
|
1964
|
-
auth_headers.update(self.zinstance.default_headers())
|
|
1965
1947
|
data = {
|
|
1966
1948
|
"md5": digest.hexdigest(),
|
|
1967
1949
|
"filename": os.path.basename(attachment),
|
|
@@ -1972,7 +1954,7 @@ class Zupload:
|
|
|
1972
1954
|
"params": 1,
|
|
1973
1955
|
}
|
|
1974
1956
|
self.zinstance._check_backoff()
|
|
1975
|
-
auth_req =
|
|
1957
|
+
auth_req = self.zinstance.client.post(
|
|
1976
1958
|
url=build_url(
|
|
1977
1959
|
self.zinstance.endpoint,
|
|
1978
1960
|
"/{t}/{u}/items/{i}/file".format(
|
|
@@ -1981,12 +1963,12 @@ class Zupload:
|
|
|
1981
1963
|
i=reg_key,
|
|
1982
1964
|
),
|
|
1983
1965
|
),
|
|
1984
|
-
|
|
1966
|
+
content=data,
|
|
1985
1967
|
headers=auth_headers,
|
|
1986
1968
|
)
|
|
1987
1969
|
try:
|
|
1988
1970
|
auth_req.raise_for_status()
|
|
1989
|
-
except
|
|
1971
|
+
except httpx.HTTPError as exc:
|
|
1990
1972
|
error_handler(self.zinstance, auth_req, exc)
|
|
1991
1973
|
backoff = auth_req.headers.get("backoff") or auth_req.headers.get("retry-after")
|
|
1992
1974
|
if backoff:
|
|
@@ -2009,16 +1991,16 @@ class Zupload:
|
|
|
2009
1991
|
upload_pairs = tuple(upload_list)
|
|
2010
1992
|
try:
|
|
2011
1993
|
self.zinstance._check_backoff()
|
|
2012
|
-
upload =
|
|
1994
|
+
upload = self.client.post(
|
|
2013
1995
|
url=authdata["url"],
|
|
2014
1996
|
files=upload_pairs,
|
|
2015
|
-
headers={"User-Agent": "Pyzotero
|
|
1997
|
+
headers={"User-Agent": f"Pyzotero/{pz.__version__}"},
|
|
2016
1998
|
)
|
|
2017
|
-
except
|
|
1999
|
+
except httpx.ConnectionError:
|
|
2018
2000
|
raise ze.UploadError("ConnectionError")
|
|
2019
2001
|
try:
|
|
2020
2002
|
upload.raise_for_status()
|
|
2021
|
-
except
|
|
2003
|
+
except httpx.HTTPError as exc:
|
|
2022
2004
|
error_handler(self.zinstance, upload, exc)
|
|
2023
2005
|
backoff = upload.headers.get("backoff") or upload.headers.get("retry-after")
|
|
2024
2006
|
if backoff:
|
|
@@ -2034,10 +2016,9 @@ class Zupload:
|
|
|
2034
2016
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
2035
2017
|
"If-None-Match": "*",
|
|
2036
2018
|
}
|
|
2037
|
-
reg_headers.update(self.zinstance.default_headers())
|
|
2038
2019
|
reg_data = {"upload": authdata.get("uploadKey")}
|
|
2039
2020
|
self.zinstance._check_backoff()
|
|
2040
|
-
upload_reg =
|
|
2021
|
+
upload_reg = self.zinstane.client.post(
|
|
2041
2022
|
url=build_url(
|
|
2042
2023
|
self.zinstance.endpoint,
|
|
2043
2024
|
"/{t}/{u}/items/{i}/file".format(
|
|
@@ -2046,12 +2027,12 @@ class Zupload:
|
|
|
2046
2027
|
i=reg_key,
|
|
2047
2028
|
),
|
|
2048
2029
|
),
|
|
2049
|
-
|
|
2030
|
+
content=reg_data,
|
|
2050
2031
|
headers=dict(reg_headers),
|
|
2051
2032
|
)
|
|
2052
2033
|
try:
|
|
2053
2034
|
upload_reg.raise_for_status()
|
|
2054
|
-
except
|
|
2035
|
+
except httpx.HTTPError as exc:
|
|
2055
2036
|
error_handler(self.zinstance, upload_reg, exc)
|
|
2056
2037
|
backoff = upload_reg.headers.get("backoff") or upload_reg.headers.get(
|
|
2057
2038
|
"retry-after"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyzotero
|
|
3
|
-
Version: 1.6.
|
|
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
|
[](https://pypi.python.org/pypi/Pyzotero/) [](http://pyzotero.readthedocs.org/en/latest/?badge=latest) [](https://pypi.python.org/pypi/Pyzotero) [](https://anaconda.org/conda-forge/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
|
|
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 [
|
|
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 [
|
|
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/
|
|
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,,
|
pyzotero-1.6.1.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|