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 +2 -2
- pyzotero/zotero.py +134 -128
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.3.dist-info}/METADATA +10 -9
- pyzotero-1.6.3.dist-info/RECORD +10 -0
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.3.dist-info}/WHEEL +1 -1
- pyzotero-1.6.1.dist-info/RECORD +0 -10
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.3.dist-info}/AUTHORS +0 -0
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.3.dist-info}/LICENSE.md +0 -0
- {pyzotero-1.6.1.dist-info → pyzotero-1.6.3.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
|
|
|
@@ -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
|
-
)
|
|
117
|
+
)
|
|
118
|
+
with httpx.Client() as client:
|
|
119
|
+
response = client.send(r)
|
|
120
|
+
|
|
101
121
|
# now split up the URL
|
|
102
|
-
result = urlparse(
|
|
122
|
+
result = urlparse(str(response.url))
|
|
103
123
|
# construct cache key
|
|
104
|
-
cachekey = result.path
|
|
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
|
|
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
|
|
385
|
-
"Zotero-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
|
|
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.
|
|
446
|
-
|
|
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
|
|
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 = "{
|
|
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 =
|
|
555
|
+
req = self.client.get(query, headers=headers)
|
|
517
556
|
try:
|
|
518
557
|
req.raise_for_status()
|
|
519
|
-
except
|
|
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 =
|
|
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:
|
|
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 =
|
|
679
|
+
headers = {}
|
|
642
680
|
headers.update({"Content-Type": "application/json"})
|
|
643
|
-
return
|
|
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
|
-
|
|
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
|
|
660
|
-
t=self.library_type, u=self.library_id
|
|
697
|
+
query_string = "/{t}/{u}/fulltext".format(
|
|
698
|
+
t=self.library_type, u=self.library_id
|
|
661
699
|
)
|
|
662
|
-
headers =
|
|
700
|
+
headers = {}
|
|
701
|
+
params = {"since": since}
|
|
663
702
|
self._check_backoff()
|
|
664
|
-
resp =
|
|
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
|
|
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 = "{}_{
|
|
988
|
-
|
|
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 =
|
|
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
|
-
|
|
1100
|
+
content=json.dumps(payload),
|
|
1062
1101
|
)
|
|
1063
1102
|
self.request = req
|
|
1064
1103
|
try:
|
|
1065
1104
|
req.raise_for_status()
|
|
1066
|
-
except
|
|
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 =
|
|
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
|
|
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": "
|
|
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
|
-
)
|
|
1175
|
+
)
|
|
1176
|
+
with httpx.Client() as client:
|
|
1177
|
+
response = client.send(r)
|
|
1138
1178
|
# now split up the URL
|
|
1139
|
-
result = urlparse(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1377
|
+
content=json.dumps(payload),
|
|
1342
1378
|
)
|
|
1343
1379
|
self.request = req
|
|
1344
1380
|
try:
|
|
1345
1381
|
req.raise_for_status()
|
|
1346
|
-
except
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 '
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1942
|
+
content=to_send,
|
|
1935
1943
|
headers=headers,
|
|
1936
1944
|
)
|
|
1937
1945
|
try:
|
|
1938
1946
|
req.raise_for_status()
|
|
1939
|
-
except
|
|
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 =
|
|
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
|
-
|
|
1991
|
+
content=data,
|
|
1985
1992
|
headers=auth_headers,
|
|
1986
1993
|
)
|
|
1987
1994
|
try:
|
|
1988
1995
|
auth_req.raise_for_status()
|
|
1989
|
-
except
|
|
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 =
|
|
2019
|
+
upload = self.client.post(
|
|
2013
2020
|
url=authdata["url"],
|
|
2014
2021
|
files=upload_pairs,
|
|
2015
|
-
headers={"User-Agent": "Pyzotero
|
|
2022
|
+
headers={"User-Agent": f"Pyzotero/{pz.__version__}"},
|
|
2016
2023
|
)
|
|
2017
|
-
except
|
|
2024
|
+
except httpx.ConnectionError:
|
|
2018
2025
|
raise ze.UploadError("ConnectionError")
|
|
2019
2026
|
try:
|
|
2020
2027
|
upload.raise_for_status()
|
|
2021
|
-
except
|
|
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 =
|
|
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
|
-
|
|
2055
|
+
content=reg_data,
|
|
2050
2056
|
headers=dict(reg_headers),
|
|
2051
2057
|
)
|
|
2052
2058
|
try:
|
|
2053
2059
|
upload_reg.raise_for_status()
|
|
2054
|
-
except
|
|
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.
|
|
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.
|
|
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
|
[](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=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,,
|
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
|