pyzotero 1.6.0__py3-none-any.whl → 1.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _version.py +2 -2
- pyzotero/zotero.py +118 -134
- {pyzotero-1.6.0.dist-info → pyzotero-1.6.2.dist-info}/METADATA +8 -7
- pyzotero-1.6.2.dist-info/RECORD +10 -0
- pyzotero-1.6.0.dist-info/RECORD +0 -10
- {pyzotero-1.6.0.dist-info → pyzotero-1.6.2.dist-info}/AUTHORS +0 -0
- {pyzotero-1.6.0.dist-info → pyzotero-1.6.2.dist-info}/LICENSE.md +0 -0
- {pyzotero-1.6.0.dist-info → pyzotero-1.6.2.dist-info}/WHEEL +0 -0
- {pyzotero-1.6.0.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):
|
|
@@ -338,6 +348,7 @@ class Zotero:
|
|
|
338
348
|
return False
|
|
339
349
|
|
|
340
350
|
def _striplocal(self, url):
|
|
351
|
+
"""We need to remve the leading "/api" substring from urls if we're running in local mode"""
|
|
341
352
|
if self.local:
|
|
342
353
|
parsed = urlparse(url)
|
|
343
354
|
purepath = PurePosixPath(unquote(parsed.path))
|
|
@@ -380,11 +391,11 @@ class Zotero:
|
|
|
380
391
|
It's always OK to include these headers
|
|
381
392
|
"""
|
|
382
393
|
_headers = {
|
|
383
|
-
"User-Agent": "Pyzotero
|
|
384
|
-
"Zotero-API-Version": "
|
|
394
|
+
"User-Agent": f"Pyzotero/{pz.__version__}",
|
|
395
|
+
"Zotero-API-Version": f"{__api_version__}",
|
|
385
396
|
}
|
|
386
397
|
if self.api_key:
|
|
387
|
-
_headers["Authorization"] = "Bearer
|
|
398
|
+
_headers["Authorization"] = f"Bearer {self.api_key}"
|
|
388
399
|
return _headers
|
|
389
400
|
|
|
390
401
|
def _cache(self, response, key):
|
|
@@ -432,21 +443,26 @@ class Zotero:
|
|
|
432
443
|
# ensure that we wait if there's an active backoff
|
|
433
444
|
self._check_backoff()
|
|
434
445
|
# don't set locale if the url already contains it
|
|
435
|
-
if
|
|
436
|
-
|
|
446
|
+
# we always add a locale if it's a "standalone" or first call
|
|
447
|
+
needs_locale = not self.links or not self._check_for_component(
|
|
448
|
+
self.links.get("next"), "locale"
|
|
449
|
+
)
|
|
450
|
+
if needs_locale:
|
|
451
|
+
if params:
|
|
437
452
|
params["locale"] = self.locale
|
|
438
|
-
if not params and self.links:
|
|
439
|
-
if not self._check_for_component(self.links.get("next"), "locale"):
|
|
440
|
-
params = {"locale": self.locale}
|
|
441
453
|
else:
|
|
442
|
-
params = {}
|
|
443
|
-
self.
|
|
444
|
-
|
|
445
|
-
|
|
454
|
+
params = {"locale": self.locale}
|
|
455
|
+
# we now have to merge self.url_params (default params, and those supplied by the user)
|
|
456
|
+
if not params:
|
|
457
|
+
params = {}
|
|
458
|
+
if not self.url_params:
|
|
459
|
+
self.url_params = {}
|
|
460
|
+
merged_params = params | self.url_params
|
|
461
|
+
self.request = self.client.get(url=full_url, params=merged_params)
|
|
446
462
|
self.request.encoding = "utf-8"
|
|
447
463
|
try:
|
|
448
464
|
self.request.raise_for_status()
|
|
449
|
-
except
|
|
465
|
+
except httpx.HTTPError as exc:
|
|
450
466
|
error_handler(self, self.request, exc)
|
|
451
467
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
452
468
|
"retry-after"
|
|
@@ -463,7 +479,7 @@ class Zotero:
|
|
|
463
479
|
try:
|
|
464
480
|
for key, value in self.request.links.items():
|
|
465
481
|
parsed = urlparse(value["url"])
|
|
466
|
-
fragment = "{
|
|
482
|
+
fragment = f"{parsed[2]}?{parsed[4]}"
|
|
467
483
|
extracted[key] = fragment
|
|
468
484
|
# add a 'self' link
|
|
469
485
|
parsed = list(urlparse(self.self_link))
|
|
@@ -508,13 +524,12 @@ class Zotero:
|
|
|
508
524
|
"%a, %d %b %Y %H:%M:%S %Z"
|
|
509
525
|
)
|
|
510
526
|
}
|
|
511
|
-
headers.update(self.default_headers())
|
|
512
527
|
# perform the request, and check whether the response returns 304
|
|
513
528
|
self._check_backoff()
|
|
514
|
-
req =
|
|
529
|
+
req = self.client.get(query, headers=headers)
|
|
515
530
|
try:
|
|
516
531
|
req.raise_for_status()
|
|
517
|
-
except
|
|
532
|
+
except httpx.HTTPError as exc:
|
|
518
533
|
error_handler(self, req, exc)
|
|
519
534
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
520
535
|
"retry-after"
|
|
@@ -548,7 +563,7 @@ class Zotero:
|
|
|
548
563
|
# bib format can't have a limit
|
|
549
564
|
if params.get("format") == "bib":
|
|
550
565
|
del params["limit"]
|
|
551
|
-
self.url_params =
|
|
566
|
+
self.url_params = params
|
|
552
567
|
|
|
553
568
|
def _build_query(self, query_string, no_params=False):
|
|
554
569
|
"""
|
|
@@ -558,12 +573,11 @@ class Zotero:
|
|
|
558
573
|
try:
|
|
559
574
|
query = quote(query_string.format(u=self.library_id, t=self.library_type))
|
|
560
575
|
except KeyError as err:
|
|
561
|
-
raise ze.ParamNotPassed("There's a request parameter missing:
|
|
576
|
+
raise ze.ParamNotPassed(f"There's a request parameter missing: {err}")
|
|
562
577
|
# Add the URL parameters and the user key, if necessary
|
|
563
578
|
if no_params is False:
|
|
564
579
|
if not self.url_params:
|
|
565
580
|
self.add_parameters()
|
|
566
|
-
query = "%s?%s" % (query, self.url_params)
|
|
567
581
|
return query
|
|
568
582
|
|
|
569
583
|
@retrieve
|
|
@@ -636,9 +650,9 @@ class Zotero:
|
|
|
636
650
|
For text documents, 'indexedChars' and 'totalChars' OR
|
|
637
651
|
For PDFs, 'indexedPages' and 'totalPages'.
|
|
638
652
|
"""
|
|
639
|
-
headers =
|
|
653
|
+
headers = {}
|
|
640
654
|
headers.update({"Content-Type": "application/json"})
|
|
641
|
-
return
|
|
655
|
+
return self.client.put(
|
|
642
656
|
url=build_url(
|
|
643
657
|
self.endpoint,
|
|
644
658
|
"/{t}/{u}/items/{k}/fulltext".format(
|
|
@@ -646,7 +660,7 @@ class Zotero:
|
|
|
646
660
|
),
|
|
647
661
|
),
|
|
648
662
|
headers=headers,
|
|
649
|
-
|
|
663
|
+
content=json.dumps(payload),
|
|
650
664
|
)
|
|
651
665
|
|
|
652
666
|
def new_fulltext(self, since):
|
|
@@ -654,15 +668,18 @@ class Zotero:
|
|
|
654
668
|
Retrieve list of full-text content items and versions which are newer
|
|
655
669
|
than <since>
|
|
656
670
|
"""
|
|
657
|
-
query_string = "/{t}/{u}/fulltext
|
|
658
|
-
t=self.library_type, u=self.library_id
|
|
671
|
+
query_string = "/{t}/{u}/fulltext".format(
|
|
672
|
+
t=self.library_type, u=self.library_id
|
|
659
673
|
)
|
|
660
|
-
headers =
|
|
674
|
+
headers = {}
|
|
675
|
+
params = {"since": since}
|
|
661
676
|
self._check_backoff()
|
|
662
|
-
resp =
|
|
677
|
+
resp = self.client.get(
|
|
678
|
+
build_url(self.endpoint, query_string), params=params, headers=headers
|
|
679
|
+
)
|
|
663
680
|
try:
|
|
664
681
|
resp.raise_for_status()
|
|
665
|
-
except
|
|
682
|
+
except httpx.HTTPError as exc:
|
|
666
683
|
error_handler(self, resp, exc)
|
|
667
684
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
668
685
|
"retry-after"
|
|
@@ -877,6 +894,7 @@ class Zotero:
|
|
|
877
894
|
"""Return the result of the call to the URL in the 'Next' link"""
|
|
878
895
|
if n := self.links.get("next"):
|
|
879
896
|
newurl = self._striplocal(n)
|
|
897
|
+
print(newurl)
|
|
880
898
|
return newurl
|
|
881
899
|
return
|
|
882
900
|
|
|
@@ -981,17 +999,17 @@ class Zotero:
|
|
|
981
999
|
def item_template(self, itemtype, linkmode=None):
|
|
982
1000
|
"""Get a template for a new item"""
|
|
983
1001
|
# if we have a template and it hasn't been updated since we stored it
|
|
984
|
-
template_name = "{}_{
|
|
985
|
-
|
|
1002
|
+
template_name = f"item_template_{itemtype}_{linkmode or ''}"
|
|
1003
|
+
params = {"itemType": itemtype}
|
|
1004
|
+
# Set linkMode parameter for API request if itemtype is attachment
|
|
1005
|
+
if itemtype == "attachment":
|
|
1006
|
+
params["linkMode"] = linkmode
|
|
1007
|
+
self.add_parameters(**params)
|
|
1008
|
+
query_string = "/items/new"
|
|
986
1009
|
if self.templates.get(template_name) and not self._updated(
|
|
987
1010
|
query_string, self.templates[template_name], template_name
|
|
988
1011
|
):
|
|
989
1012
|
return copy.deepcopy(self.templates[template_name]["tmplt"])
|
|
990
|
-
|
|
991
|
-
# Set linkMode parameter for API request if itemtype is attachment
|
|
992
|
-
if itemtype == "attachment":
|
|
993
|
-
query_string = "{}&linkMode={}".format(query_string, linkmode)
|
|
994
|
-
|
|
995
1013
|
# otherwise perform a normal request and cache the response
|
|
996
1014
|
retrieved = self._retrieve_data(query_string)
|
|
997
1015
|
return self._cache(retrieved, template_name)
|
|
@@ -1047,20 +1065,19 @@ class Zotero:
|
|
|
1047
1065
|
self.savedsearch._validate(conditions)
|
|
1048
1066
|
payload = [{"name": name, "conditions": conditions}]
|
|
1049
1067
|
headers = {"Zotero-Write-Token": token()}
|
|
1050
|
-
headers.update(self.default_headers())
|
|
1051
1068
|
self._check_backoff()
|
|
1052
|
-
req =
|
|
1069
|
+
req = self.client.post(
|
|
1053
1070
|
url=build_url(
|
|
1054
1071
|
self.endpoint,
|
|
1055
1072
|
"/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
|
|
1056
1073
|
),
|
|
1057
1074
|
headers=headers,
|
|
1058
|
-
|
|
1075
|
+
content=json.dumps(payload),
|
|
1059
1076
|
)
|
|
1060
1077
|
self.request = req
|
|
1061
1078
|
try:
|
|
1062
1079
|
req.raise_for_status()
|
|
1063
|
-
except
|
|
1080
|
+
except httpx.HTTPError as exc:
|
|
1064
1081
|
error_handler(self, req, exc)
|
|
1065
1082
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
1066
1083
|
"retry-after"
|
|
@@ -1075,9 +1092,8 @@ class Zotero:
|
|
|
1075
1092
|
unique search keys
|
|
1076
1093
|
"""
|
|
1077
1094
|
headers = {"Zotero-Write-Token": token()}
|
|
1078
|
-
headers.update(self.default_headers())
|
|
1079
1095
|
self._check_backoff()
|
|
1080
|
-
req =
|
|
1096
|
+
req = self.client.delete(
|
|
1081
1097
|
url=build_url(
|
|
1082
1098
|
self.endpoint,
|
|
1083
1099
|
"/{t}/{u}/searches".format(t=self.library_type, u=self.library_id),
|
|
@@ -1088,7 +1104,7 @@ class Zotero:
|
|
|
1088
1104
|
self.request = req
|
|
1089
1105
|
try:
|
|
1090
1106
|
req.raise_for_status()
|
|
1091
|
-
except
|
|
1107
|
+
except httpx.HTTPError as exc:
|
|
1092
1108
|
error_handler(self, req, exc)
|
|
1093
1109
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
1094
1110
|
"retry-after"
|
|
@@ -1114,7 +1130,7 @@ class Zotero:
|
|
|
1114
1130
|
except AssertionError:
|
|
1115
1131
|
item["data"]["tags"] = list()
|
|
1116
1132
|
for tag in tags:
|
|
1117
|
-
item["data"]["tags"].append({"tag": "
|
|
1133
|
+
item["data"]["tags"].append({"tag": f"{tag}"})
|
|
1118
1134
|
# make sure everything's OK
|
|
1119
1135
|
assert self.check_items([item])
|
|
1120
1136
|
return self.update_item(item)
|
|
@@ -1131,9 +1147,11 @@ class Zotero:
|
|
|
1131
1147
|
"GET",
|
|
1132
1148
|
build_url(self.endpoint, query_string),
|
|
1133
1149
|
params=params,
|
|
1134
|
-
)
|
|
1150
|
+
)
|
|
1151
|
+
with httpx.Client() as client:
|
|
1152
|
+
response = client.send(r)
|
|
1135
1153
|
# now split up the URL
|
|
1136
|
-
result = urlparse(
|
|
1154
|
+
result = urlparse(str(response.url))
|
|
1137
1155
|
# construct cache key
|
|
1138
1156
|
cachekey = result.path + "_" + result.query
|
|
1139
1157
|
if self.templates.get(cachekey) and not self._updated(
|
|
@@ -1186,8 +1204,7 @@ class Zotero:
|
|
|
1186
1204
|
difference = to_check.difference(template)
|
|
1187
1205
|
if difference:
|
|
1188
1206
|
raise ze.InvalidItemFields(
|
|
1189
|
-
"Invalid keys present in item
|
|
1190
|
-
% (pos + 1, " ".join(i for i in difference))
|
|
1207
|
+
f"Invalid keys present in item {pos + 1}: {' '.join(i for i in difference)}"
|
|
1191
1208
|
)
|
|
1192
1209
|
return items
|
|
1193
1210
|
|
|
@@ -1251,20 +1268,19 @@ class Zotero:
|
|
|
1251
1268
|
if last_modified is not None:
|
|
1252
1269
|
headers["If-Unmodified-Since-Version"] = str(last_modified)
|
|
1253
1270
|
to_send = json.dumps([i for i in self._cleanup(*payload, allow=("key"))])
|
|
1254
|
-
headers.update(self.default_headers())
|
|
1255
1271
|
self._check_backoff()
|
|
1256
|
-
req =
|
|
1272
|
+
req = self.client.post(
|
|
1257
1273
|
url=build_url(
|
|
1258
1274
|
self.endpoint,
|
|
1259
1275
|
"/{t}/{u}/items".format(t=self.library_type, u=self.library_id),
|
|
1260
1276
|
),
|
|
1261
|
-
|
|
1277
|
+
content=to_send,
|
|
1262
1278
|
headers=dict(headers),
|
|
1263
1279
|
)
|
|
1264
1280
|
self.request = req
|
|
1265
1281
|
try:
|
|
1266
1282
|
req.raise_for_status()
|
|
1267
|
-
except
|
|
1283
|
+
except httpx.HTTPError as exc:
|
|
1268
1284
|
error_handler(self, req, exc)
|
|
1269
1285
|
resp = req.json()
|
|
1270
1286
|
backoff = self.request.headers.get("backoff") or self.request.headers.get(
|
|
@@ -1279,24 +1295,23 @@ class Zotero:
|
|
|
1279
1295
|
uheaders = {
|
|
1280
1296
|
"If-Unmodified-Since-Version": req.headers["last-modified-version"]
|
|
1281
1297
|
}
|
|
1282
|
-
uheaders.update(self.default_headers())
|
|
1283
1298
|
for value in resp["success"].values():
|
|
1284
1299
|
payload = json.dumps({"parentItem": parentid})
|
|
1285
1300
|
self._check_backoff()
|
|
1286
|
-
presp =
|
|
1301
|
+
presp = self.client.patch(
|
|
1287
1302
|
url=build_url(
|
|
1288
1303
|
self.endpoint,
|
|
1289
1304
|
"/{t}/{u}/items/{v}".format(
|
|
1290
1305
|
t=self.library_type, u=self.library_id, v=value
|
|
1291
1306
|
),
|
|
1292
1307
|
),
|
|
1293
|
-
|
|
1308
|
+
content=payload,
|
|
1294
1309
|
headers=dict(uheaders),
|
|
1295
1310
|
)
|
|
1296
1311
|
self.request = presp
|
|
1297
1312
|
try:
|
|
1298
1313
|
presp.raise_for_status()
|
|
1299
|
-
except
|
|
1314
|
+
except httpx.HTTPError as exc:
|
|
1300
1315
|
error_handler(self, presp, exc)
|
|
1301
1316
|
backoff = presp.headers.get("backoff") or presp.headers.get(
|
|
1302
1317
|
"retry-after"
|
|
@@ -1327,20 +1342,19 @@ class Zotero:
|
|
|
1327
1342
|
headers = {"Zotero-Write-Token": token()}
|
|
1328
1343
|
if last_modified is not None:
|
|
1329
1344
|
headers["If-Unmodified-Since-Version"] = str(last_modified)
|
|
1330
|
-
headers.update(self.default_headers())
|
|
1331
1345
|
self._check_backoff()
|
|
1332
|
-
req =
|
|
1346
|
+
req = self.client.post(
|
|
1333
1347
|
url=build_url(
|
|
1334
1348
|
self.endpoint,
|
|
1335
1349
|
"/{t}/{u}/collections".format(t=self.library_type, u=self.library_id),
|
|
1336
1350
|
),
|
|
1337
1351
|
headers=headers,
|
|
1338
|
-
|
|
1352
|
+
content=json.dumps(payload),
|
|
1339
1353
|
)
|
|
1340
1354
|
self.request = req
|
|
1341
1355
|
try:
|
|
1342
1356
|
req.raise_for_status()
|
|
1343
|
-
except
|
|
1357
|
+
except httpx.HTTPError as exc:
|
|
1344
1358
|
error_handler(self, req, exc)
|
|
1345
1359
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1346
1360
|
if backoff:
|
|
@@ -1359,9 +1373,8 @@ class Zotero:
|
|
|
1359
1373
|
modified = last_modified
|
|
1360
1374
|
key = payload["key"]
|
|
1361
1375
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1362
|
-
headers.update(self.default_headers())
|
|
1363
1376
|
headers.update({"Content-Type": "application/json"})
|
|
1364
|
-
return
|
|
1377
|
+
return self.client.put(
|
|
1365
1378
|
url=build_url(
|
|
1366
1379
|
self.endpoint,
|
|
1367
1380
|
"/{t}/{u}/collections/{c}".format(
|
|
@@ -1369,7 +1382,7 @@ class Zotero:
|
|
|
1369
1382
|
),
|
|
1370
1383
|
),
|
|
1371
1384
|
headers=headers,
|
|
1372
|
-
|
|
1385
|
+
content=json.dumps(payload),
|
|
1373
1386
|
)
|
|
1374
1387
|
|
|
1375
1388
|
def attachment_simple(self, files, parentid=None):
|
|
@@ -1419,8 +1432,7 @@ class Zotero:
|
|
|
1419
1432
|
modified = last_modified
|
|
1420
1433
|
ident = payload["key"]
|
|
1421
1434
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1422
|
-
|
|
1423
|
-
return requests.patch(
|
|
1435
|
+
return self.client.patch(
|
|
1424
1436
|
url=build_url(
|
|
1425
1437
|
self.endpoint,
|
|
1426
1438
|
"/{t}/{u}/items/{id}".format(
|
|
@@ -1428,7 +1440,7 @@ class Zotero:
|
|
|
1428
1440
|
),
|
|
1429
1441
|
),
|
|
1430
1442
|
headers=headers,
|
|
1431
|
-
|
|
1443
|
+
content=json.dumps(to_send),
|
|
1432
1444
|
)
|
|
1433
1445
|
|
|
1434
1446
|
def update_items(self, payload):
|
|
@@ -1437,24 +1449,21 @@ class Zotero:
|
|
|
1437
1449
|
Accepts one argument, a list of dicts containing Item data
|
|
1438
1450
|
"""
|
|
1439
1451
|
to_send = [self.check_items([p])[0] for p in payload]
|
|
1440
|
-
headers = {}
|
|
1441
|
-
headers.update(self.default_headers())
|
|
1442
1452
|
# the API only accepts 50 items at a time, so we have to split
|
|
1443
1453
|
# anything longer
|
|
1444
1454
|
for chunk in chunks(to_send, 50):
|
|
1445
1455
|
self._check_backoff()
|
|
1446
|
-
req =
|
|
1456
|
+
req = self.client.post(
|
|
1447
1457
|
url=build_url(
|
|
1448
1458
|
self.endpoint,
|
|
1449
1459
|
"/{t}/{u}/items/".format(t=self.library_type, u=self.library_id),
|
|
1450
1460
|
),
|
|
1451
|
-
|
|
1452
|
-
data=json.dumps(chunk),
|
|
1461
|
+
content=json.dumps(chunk),
|
|
1453
1462
|
)
|
|
1454
1463
|
self.request = req
|
|
1455
1464
|
try:
|
|
1456
1465
|
req.raise_for_status()
|
|
1457
|
-
except
|
|
1466
|
+
except httpx.HTTPError as exc:
|
|
1458
1467
|
error_handler(self, req, exc)
|
|
1459
1468
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1460
1469
|
if backoff:
|
|
@@ -1467,26 +1476,23 @@ class Zotero:
|
|
|
1467
1476
|
Accepts one argument, a list of dicts containing Collection data
|
|
1468
1477
|
"""
|
|
1469
1478
|
to_send = [self.check_items([p])[0] for p in payload]
|
|
1470
|
-
headers = {}
|
|
1471
|
-
headers.update(self.default_headers())
|
|
1472
1479
|
# the API only accepts 50 items at a time, so we have to split
|
|
1473
1480
|
# anything longer
|
|
1474
1481
|
for chunk in chunks(to_send, 50):
|
|
1475
1482
|
self._check_backoff()
|
|
1476
|
-
req =
|
|
1483
|
+
req = self.client.post(
|
|
1477
1484
|
url=build_url(
|
|
1478
1485
|
self.endpoint,
|
|
1479
1486
|
"/{t}/{u}/collections/".format(
|
|
1480
1487
|
t=self.library_type, u=self.library_id
|
|
1481
1488
|
),
|
|
1482
1489
|
),
|
|
1483
|
-
|
|
1484
|
-
data=json.dumps(chunk),
|
|
1490
|
+
content=json.dumps(chunk),
|
|
1485
1491
|
)
|
|
1486
1492
|
self.request = req
|
|
1487
1493
|
try:
|
|
1488
1494
|
req.raise_for_status()
|
|
1489
|
-
except
|
|
1495
|
+
except httpx.HTTPError as exc:
|
|
1490
1496
|
error_handler(self, req, exc)
|
|
1491
1497
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1492
1498
|
if backoff:
|
|
@@ -1505,15 +1511,14 @@ class Zotero:
|
|
|
1505
1511
|
# add the collection data from the item
|
|
1506
1512
|
modified_collections = payload["data"]["collections"] + [collection]
|
|
1507
1513
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1508
|
-
|
|
1509
|
-
return requests.patch(
|
|
1514
|
+
return self.client.patch(
|
|
1510
1515
|
url=build_url(
|
|
1511
1516
|
self.endpoint,
|
|
1512
1517
|
"/{t}/{u}/items/{i}".format(
|
|
1513
1518
|
t=self.library_type, u=self.library_id, i=ident
|
|
1514
1519
|
),
|
|
1515
1520
|
),
|
|
1516
|
-
|
|
1521
|
+
content=json.dumps({"collections": modified_collections}),
|
|
1517
1522
|
headers=headers,
|
|
1518
1523
|
)
|
|
1519
1524
|
|
|
@@ -1531,15 +1536,14 @@ class Zotero:
|
|
|
1531
1536
|
c for c in payload["data"]["collections"] if c != collection
|
|
1532
1537
|
]
|
|
1533
1538
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1534
|
-
|
|
1535
|
-
return requests.patch(
|
|
1539
|
+
return self.client.patch(
|
|
1536
1540
|
url=build_url(
|
|
1537
1541
|
self.endpoint,
|
|
1538
1542
|
"/{t}/{u}/items/{i}".format(
|
|
1539
1543
|
t=self.library_type, u=self.library_id, i=ident
|
|
1540
1544
|
),
|
|
1541
1545
|
),
|
|
1542
|
-
|
|
1546
|
+
content=json.dumps({"collections": modified_collections}),
|
|
1543
1547
|
headers=headers,
|
|
1544
1548
|
)
|
|
1545
1549
|
|
|
@@ -1558,8 +1562,7 @@ class Zotero:
|
|
|
1558
1562
|
headers = {
|
|
1559
1563
|
"If-Unmodified-Since-Version": self.request.headers["last-modified-version"]
|
|
1560
1564
|
}
|
|
1561
|
-
|
|
1562
|
-
return requests.delete(
|
|
1565
|
+
return self.client.delete(
|
|
1563
1566
|
url=build_url(
|
|
1564
1567
|
self.endpoint,
|
|
1565
1568
|
"/{t}/{u}/tags".format(t=self.library_type, u=self.library_id),
|
|
@@ -1600,8 +1603,7 @@ class Zotero:
|
|
|
1600
1603
|
),
|
|
1601
1604
|
)
|
|
1602
1605
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1603
|
-
|
|
1604
|
-
return requests.delete(url=url, params=params, headers=headers)
|
|
1606
|
+
return self.client.delete(url=url, params=params, headers=headers)
|
|
1605
1607
|
|
|
1606
1608
|
@backoff_check
|
|
1607
1609
|
def delete_collection(self, payload, last_modified=None):
|
|
@@ -1635,8 +1637,7 @@ class Zotero:
|
|
|
1635
1637
|
),
|
|
1636
1638
|
)
|
|
1637
1639
|
headers = {"If-Unmodified-Since-Version": str(modified)}
|
|
1638
|
-
|
|
1639
|
-
return requests.delete(url=url, params=params, headers=headers)
|
|
1640
|
+
return self.client.delete(url=url, params=params, headers=headers)
|
|
1640
1641
|
|
|
1641
1642
|
|
|
1642
1643
|
def error_handler(zot, req, exc=None):
|
|
@@ -1655,13 +1656,7 @@ def error_handler(zot, req, exc=None):
|
|
|
1655
1656
|
|
|
1656
1657
|
def err_msg(req):
|
|
1657
1658
|
"""Return a nicely-formatted error message"""
|
|
1658
|
-
return "\nCode:
|
|
1659
|
-
req.status_code,
|
|
1660
|
-
# error.msg,
|
|
1661
|
-
req.url,
|
|
1662
|
-
req.request.method,
|
|
1663
|
-
req.text,
|
|
1664
|
-
)
|
|
1659
|
+
return f"\nCode: {req.status_code}\nURL: {str(req.url)}\nMethod: {req.request.method}\nResponse: {req.text}"
|
|
1665
1660
|
|
|
1666
1661
|
if error_codes.get(req.status_code):
|
|
1667
1662
|
# check to see whether its 429
|
|
@@ -1830,12 +1825,11 @@ class SavedSearch:
|
|
|
1830
1825
|
for condition in conditions:
|
|
1831
1826
|
if set(condition.keys()) != allowed_keys:
|
|
1832
1827
|
raise ze.ParamNotPassed(
|
|
1833
|
-
"Keys must be all of:
|
|
1828
|
+
f"Keys must be all of: {', '.join(self.searchkeys)}"
|
|
1834
1829
|
)
|
|
1835
1830
|
if condition.get("operator") not in operators_set:
|
|
1836
1831
|
raise ze.ParamNotPassed(
|
|
1837
|
-
"You have specified an unknown operator:
|
|
1838
|
-
% condition.get("operator")
|
|
1832
|
+
f"You have specified an unknown operator: {condition.get('operator')}"
|
|
1839
1833
|
)
|
|
1840
1834
|
# dict keys of allowed operators for the current condition
|
|
1841
1835
|
permitted_operators = self.conditions_operators.get(
|
|
@@ -1847,12 +1841,7 @@ class SavedSearch:
|
|
|
1847
1841
|
)
|
|
1848
1842
|
if condition.get("operator") not in permitted_operators_list:
|
|
1849
1843
|
raise ze.ParamNotPassed(
|
|
1850
|
-
"You may not use the '
|
|
1851
|
-
% (
|
|
1852
|
-
condition.get("operator"),
|
|
1853
|
-
condition.get("condition"),
|
|
1854
|
-
", ".join(list(permitted_operators_list)),
|
|
1855
|
-
)
|
|
1844
|
+
f"You may not use the '{condition.get('operator')}' operator when selecting the '{condition.get('condition')}' condition. \nAllowed operators: {', '.join(list(permitted_operators_list))}"
|
|
1856
1845
|
)
|
|
1857
1846
|
|
|
1858
1847
|
|
|
@@ -1890,14 +1879,12 @@ class Zupload:
|
|
|
1890
1879
|
pass
|
|
1891
1880
|
except IOError:
|
|
1892
1881
|
raise ze.FileDoesNotExist(
|
|
1893
|
-
"The file at
|
|
1894
|
-
% str(self.basedir.joinpath(templt["filename"]))
|
|
1882
|
+
f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
|
|
1895
1883
|
)
|
|
1896
1884
|
# no point in continuing if the file isn't a file
|
|
1897
1885
|
else:
|
|
1898
1886
|
raise ze.FileDoesNotExist(
|
|
1899
|
-
"The file at
|
|
1900
|
-
% str(self.basedir.joinpath(templt["filename"]))
|
|
1887
|
+
f"The file at {str(self.basedir.joinpath(templt['filename']))} couldn't be opened or found."
|
|
1901
1888
|
)
|
|
1902
1889
|
|
|
1903
1890
|
def _create_prelim(self):
|
|
@@ -1914,26 +1901,25 @@ class Zupload:
|
|
|
1914
1901
|
liblevel = "/{t}/{u}/items"
|
|
1915
1902
|
# Create one or more new attachments
|
|
1916
1903
|
headers = {"Zotero-Write-Token": token(), "Content-Type": "application/json"}
|
|
1917
|
-
headers.update(self.zinstance.default_headers())
|
|
1918
1904
|
# If we have a Parent ID, add it as a parentItem
|
|
1919
1905
|
if self.parentid:
|
|
1920
1906
|
for child in self.payload:
|
|
1921
1907
|
child["parentItem"] = self.parentid
|
|
1922
1908
|
to_send = json.dumps(self.payload)
|
|
1923
1909
|
self.zinstance._check_backoff()
|
|
1924
|
-
req =
|
|
1910
|
+
req = self.client.post(
|
|
1925
1911
|
url=build_url(
|
|
1926
1912
|
self.zinstance.endpoint,
|
|
1927
1913
|
liblevel.format(
|
|
1928
1914
|
t=self.zinstance.library_type, u=self.zinstance.library_id
|
|
1929
1915
|
),
|
|
1930
1916
|
),
|
|
1931
|
-
|
|
1917
|
+
content=to_send,
|
|
1932
1918
|
headers=headers,
|
|
1933
1919
|
)
|
|
1934
1920
|
try:
|
|
1935
1921
|
req.raise_for_status()
|
|
1936
|
-
except
|
|
1922
|
+
except httpx.HTTPError as exc:
|
|
1937
1923
|
error_handler(self.zinstance, req, exc)
|
|
1938
1924
|
backoff = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1939
1925
|
if backoff:
|
|
@@ -1958,7 +1944,6 @@ class Zupload:
|
|
|
1958
1944
|
else:
|
|
1959
1945
|
# docs specify that for existing file we use this
|
|
1960
1946
|
auth_headers["If-Match"] = md5
|
|
1961
|
-
auth_headers.update(self.zinstance.default_headers())
|
|
1962
1947
|
data = {
|
|
1963
1948
|
"md5": digest.hexdigest(),
|
|
1964
1949
|
"filename": os.path.basename(attachment),
|
|
@@ -1969,7 +1954,7 @@ class Zupload:
|
|
|
1969
1954
|
"params": 1,
|
|
1970
1955
|
}
|
|
1971
1956
|
self.zinstance._check_backoff()
|
|
1972
|
-
auth_req =
|
|
1957
|
+
auth_req = self.zinstance.client.post(
|
|
1973
1958
|
url=build_url(
|
|
1974
1959
|
self.zinstance.endpoint,
|
|
1975
1960
|
"/{t}/{u}/items/{i}/file".format(
|
|
@@ -1978,12 +1963,12 @@ class Zupload:
|
|
|
1978
1963
|
i=reg_key,
|
|
1979
1964
|
),
|
|
1980
1965
|
),
|
|
1981
|
-
|
|
1966
|
+
content=data,
|
|
1982
1967
|
headers=auth_headers,
|
|
1983
1968
|
)
|
|
1984
1969
|
try:
|
|
1985
1970
|
auth_req.raise_for_status()
|
|
1986
|
-
except
|
|
1971
|
+
except httpx.HTTPError as exc:
|
|
1987
1972
|
error_handler(self.zinstance, auth_req, exc)
|
|
1988
1973
|
backoff = auth_req.headers.get("backoff") or auth_req.headers.get("retry-after")
|
|
1989
1974
|
if backoff:
|
|
@@ -2006,16 +1991,16 @@ class Zupload:
|
|
|
2006
1991
|
upload_pairs = tuple(upload_list)
|
|
2007
1992
|
try:
|
|
2008
1993
|
self.zinstance._check_backoff()
|
|
2009
|
-
upload =
|
|
1994
|
+
upload = self.client.post(
|
|
2010
1995
|
url=authdata["url"],
|
|
2011
1996
|
files=upload_pairs,
|
|
2012
|
-
headers={"User-Agent": "Pyzotero
|
|
1997
|
+
headers={"User-Agent": f"Pyzotero/{pz.__version__}"},
|
|
2013
1998
|
)
|
|
2014
|
-
except
|
|
1999
|
+
except httpx.ConnectionError:
|
|
2015
2000
|
raise ze.UploadError("ConnectionError")
|
|
2016
2001
|
try:
|
|
2017
2002
|
upload.raise_for_status()
|
|
2018
|
-
except
|
|
2003
|
+
except httpx.HTTPError as exc:
|
|
2019
2004
|
error_handler(self.zinstance, upload, exc)
|
|
2020
2005
|
backoff = upload.headers.get("backoff") or upload.headers.get("retry-after")
|
|
2021
2006
|
if backoff:
|
|
@@ -2031,10 +2016,9 @@ class Zupload:
|
|
|
2031
2016
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
2032
2017
|
"If-None-Match": "*",
|
|
2033
2018
|
}
|
|
2034
|
-
reg_headers.update(self.zinstance.default_headers())
|
|
2035
2019
|
reg_data = {"upload": authdata.get("uploadKey")}
|
|
2036
2020
|
self.zinstance._check_backoff()
|
|
2037
|
-
upload_reg =
|
|
2021
|
+
upload_reg = self.zinstane.client.post(
|
|
2038
2022
|
url=build_url(
|
|
2039
2023
|
self.zinstance.endpoint,
|
|
2040
2024
|
"/{t}/{u}/items/{i}/file".format(
|
|
@@ -2043,12 +2027,12 @@ class Zupload:
|
|
|
2043
2027
|
i=reg_key,
|
|
2044
2028
|
),
|
|
2045
2029
|
),
|
|
2046
|
-
|
|
2030
|
+
content=reg_data,
|
|
2047
2031
|
headers=dict(reg_headers),
|
|
2048
2032
|
)
|
|
2049
2033
|
try:
|
|
2050
2034
|
upload_reg.raise_for_status()
|
|
2051
|
-
except
|
|
2035
|
+
except httpx.HTTPError as exc:
|
|
2052
2036
|
error_handler(self.zinstance, upload_reg, exc)
|
|
2053
2037
|
backoff = upload_reg.headers.get("backoff") or upload_reg.headers.get(
|
|
2054
2038
|
"retry-after"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyzotero
|
|
3
|
-
Version: 1.6.
|
|
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.0.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
_version.py,sha256=Ry70pc5l-IBhT9gahlkNwZPp4g0CzVEWqsat9H-UASY,411
|
|
2
|
-
pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
|
|
3
|
-
pyzotero/zotero.py,sha256=qij4_ihYYzZI-18_CPZD2sexqs0Iya1E61dcaSZpmZo,76201
|
|
4
|
-
pyzotero/zotero_errors.py,sha256=UPhAmf2K05cnoeIl2wjufWQedepg7vBKb-ShU0TdlL4,2582
|
|
5
|
-
pyzotero-1.6.0.dist-info/AUTHORS,sha256=ZMicxg7lRScOYbxzMPznlzMbmrFIUIHwg-NvljEMbRQ,110
|
|
6
|
-
pyzotero-1.6.0.dist-info/LICENSE.md,sha256=bhy1CPMj1zWffD9YifFmSeBzPylsrhb1qP8OCEx5Etw,1550
|
|
7
|
-
pyzotero-1.6.0.dist-info/METADATA,sha256=vSYfI-tzsTaMafBXfjZFvPqXMrFHjW2EHB9uAepQHeE,7204
|
|
8
|
-
pyzotero-1.6.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
9
|
-
pyzotero-1.6.0.dist-info/top_level.txt,sha256=BOPNkPk5VtNDCy_li7Xftx6k0zG8STGxh-KgckcxLEw,18
|
|
10
|
-
pyzotero-1.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|