pyzotero 1.6.16__py3-none-any.whl → 1.7.0__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.
- pyzotero/cli.py +310 -0
- pyzotero/zotero.py +14 -20
- {pyzotero-1.6.16.dist-info → pyzotero-1.7.0.dist-info}/METADATA +66 -1
- pyzotero-1.7.0.dist-info/RECORD +9 -0
- {pyzotero-1.6.16.dist-info → pyzotero-1.7.0.dist-info}/WHEEL +1 -1
- pyzotero-1.7.0.dist-info/entry_points.txt +3 -0
- pyzotero-1.6.16.dist-info/RECORD +0 -7
pyzotero/cli.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Command-line interface for pyzotero."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from pyzotero import zotero
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_zotero_client(locale="en-US"):
|
|
12
|
+
"""Get a Zotero client configured for local access."""
|
|
13
|
+
return zotero.Zotero(library_id="0", library_type="user", local=True, locale=locale)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.option(
|
|
18
|
+
"--locale",
|
|
19
|
+
default="en-US",
|
|
20
|
+
help="Locale for localized strings (default: en-US)",
|
|
21
|
+
)
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def main(ctx, locale):
|
|
24
|
+
"""Search local Zotero library."""
|
|
25
|
+
ctx.ensure_object(dict)
|
|
26
|
+
ctx.obj["locale"] = locale
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@main.command()
|
|
30
|
+
@click.option(
|
|
31
|
+
"-q",
|
|
32
|
+
"--query",
|
|
33
|
+
help="Search query string",
|
|
34
|
+
default="",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--fulltext",
|
|
38
|
+
is_flag=True,
|
|
39
|
+
help="Enable full-text search (qmode='everything')",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--itemtype",
|
|
43
|
+
multiple=True,
|
|
44
|
+
help="Filter by item type (can be specified multiple times for OR search)",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--collection",
|
|
48
|
+
help="Filter by collection key (returns only items in this collection)",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--limit",
|
|
52
|
+
type=int,
|
|
53
|
+
default=1000000,
|
|
54
|
+
help="Maximum number of results to return (default: 1000000)",
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--json",
|
|
58
|
+
"output_json",
|
|
59
|
+
is_flag=True,
|
|
60
|
+
help="Output results as JSON",
|
|
61
|
+
)
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def search(ctx, query, fulltext, itemtype, collection, limit, output_json): # noqa: PLR0912, PLR0915
|
|
64
|
+
"""Search local Zotero library.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
pyzotero search -q "machine learning"
|
|
68
|
+
|
|
69
|
+
pyzotero search -q "climate change" --fulltext
|
|
70
|
+
|
|
71
|
+
pyzotero search -q "methodology" --itemtype book --itemtype journalArticle
|
|
72
|
+
|
|
73
|
+
pyzotero search --collection ABC123 -q "test"
|
|
74
|
+
|
|
75
|
+
pyzotero search -q "climate" --json
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
80
|
+
zot = _get_zotero_client(locale)
|
|
81
|
+
|
|
82
|
+
# Build query parameters
|
|
83
|
+
params = {"limit": limit}
|
|
84
|
+
|
|
85
|
+
if query:
|
|
86
|
+
params["q"] = query
|
|
87
|
+
|
|
88
|
+
if fulltext:
|
|
89
|
+
params["qmode"] = "everything"
|
|
90
|
+
|
|
91
|
+
if itemtype:
|
|
92
|
+
# Join multiple item types with || for OR search
|
|
93
|
+
params["itemType"] = " || ".join(itemtype)
|
|
94
|
+
|
|
95
|
+
# Execute search using collection_items_top() if collection specified, otherwise top()
|
|
96
|
+
if collection:
|
|
97
|
+
results = zot.collection_items_top(collection, **params)
|
|
98
|
+
else:
|
|
99
|
+
results = zot.top(**params)
|
|
100
|
+
|
|
101
|
+
# Handle empty results
|
|
102
|
+
if not results:
|
|
103
|
+
if output_json:
|
|
104
|
+
click.echo(json.dumps([]))
|
|
105
|
+
else:
|
|
106
|
+
click.echo("No results found.")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Build output data structure
|
|
110
|
+
output_items = []
|
|
111
|
+
for item in results:
|
|
112
|
+
data = item.get("data", {})
|
|
113
|
+
|
|
114
|
+
title = data.get("title", "No title")
|
|
115
|
+
item_type = data.get("itemType", "Unknown")
|
|
116
|
+
date = data.get("date", "No date")
|
|
117
|
+
item_key = data.get("key", "")
|
|
118
|
+
publication = data.get("publicationTitle", "")
|
|
119
|
+
volume = data.get("volume", "")
|
|
120
|
+
issue = data.get("issue", "")
|
|
121
|
+
doi = data.get("DOI", "")
|
|
122
|
+
url = data.get("url", "")
|
|
123
|
+
|
|
124
|
+
# Format creators (authors, editors, etc.)
|
|
125
|
+
creators = data.get("creators", [])
|
|
126
|
+
creator_names = []
|
|
127
|
+
for creator in creators:
|
|
128
|
+
if "lastName" in creator:
|
|
129
|
+
if "firstName" in creator:
|
|
130
|
+
creator_names.append(
|
|
131
|
+
f"{creator['firstName']} {creator['lastName']}"
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
creator_names.append(creator["lastName"])
|
|
135
|
+
elif "name" in creator:
|
|
136
|
+
creator_names.append(creator["name"])
|
|
137
|
+
|
|
138
|
+
# Check for PDF attachments
|
|
139
|
+
pdf_attachments = []
|
|
140
|
+
num_children = item.get("meta", {}).get("numChildren", 0)
|
|
141
|
+
if num_children > 0:
|
|
142
|
+
children = zot.children(item_key)
|
|
143
|
+
for child in children:
|
|
144
|
+
child_data = child.get("data", {})
|
|
145
|
+
if child_data.get("contentType") == "application/pdf":
|
|
146
|
+
# Extract file URL from links.enclosure.href
|
|
147
|
+
file_url = (
|
|
148
|
+
child.get("links", {}).get("enclosure", {}).get("href", "")
|
|
149
|
+
)
|
|
150
|
+
if file_url:
|
|
151
|
+
pdf_attachments.append(file_url)
|
|
152
|
+
|
|
153
|
+
# Build item object for JSON output
|
|
154
|
+
item_obj = {
|
|
155
|
+
"key": item_key,
|
|
156
|
+
"itemType": item_type,
|
|
157
|
+
"title": title,
|
|
158
|
+
"creators": creator_names,
|
|
159
|
+
"date": date,
|
|
160
|
+
"publication": publication,
|
|
161
|
+
"volume": volume,
|
|
162
|
+
"issue": issue,
|
|
163
|
+
"doi": doi,
|
|
164
|
+
"url": url,
|
|
165
|
+
"pdfAttachments": pdf_attachments,
|
|
166
|
+
}
|
|
167
|
+
output_items.append(item_obj)
|
|
168
|
+
|
|
169
|
+
# Output results
|
|
170
|
+
if output_json:
|
|
171
|
+
click.echo(json.dumps(output_items, indent=2))
|
|
172
|
+
else:
|
|
173
|
+
click.echo(f"\nFound {len(results)} items:\n")
|
|
174
|
+
for idx, item_obj in enumerate(output_items, 1):
|
|
175
|
+
authors_str = (
|
|
176
|
+
", ".join(item_obj["creators"])
|
|
177
|
+
if item_obj["creators"]
|
|
178
|
+
else "No authors"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
click.echo(f"{idx}. [{item_obj['itemType']}] {item_obj['title']}")
|
|
182
|
+
click.echo(f" Authors: {authors_str}")
|
|
183
|
+
click.echo(f" Date: {item_obj['date']}")
|
|
184
|
+
click.echo(f" Publication: {item_obj['publication']}")
|
|
185
|
+
click.echo(f" Volume: {item_obj['volume']}")
|
|
186
|
+
click.echo(f" Issue: {item_obj['issue']}")
|
|
187
|
+
click.echo(f" DOI: {item_obj['doi']}")
|
|
188
|
+
click.echo(f" URL: {item_obj['url']}")
|
|
189
|
+
click.echo(f" Key: {item_obj['key']}")
|
|
190
|
+
|
|
191
|
+
if item_obj["pdfAttachments"]:
|
|
192
|
+
click.echo(" PDF Attachments:")
|
|
193
|
+
for pdf_url in item_obj["pdfAttachments"]:
|
|
194
|
+
click.echo(f" {pdf_url}")
|
|
195
|
+
|
|
196
|
+
click.echo()
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@main.command()
|
|
204
|
+
@click.option(
|
|
205
|
+
"--limit",
|
|
206
|
+
type=int,
|
|
207
|
+
help="Maximum number of collections to return (default: all)",
|
|
208
|
+
)
|
|
209
|
+
@click.pass_context
|
|
210
|
+
def listcollections(ctx, limit):
|
|
211
|
+
"""List all collections in the local Zotero library.
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
pyzotero listcollections
|
|
215
|
+
|
|
216
|
+
pyzotero listcollections --limit 10
|
|
217
|
+
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
221
|
+
zot = _get_zotero_client(locale)
|
|
222
|
+
|
|
223
|
+
# Build query parameters
|
|
224
|
+
params = {}
|
|
225
|
+
if limit:
|
|
226
|
+
params["limit"] = limit
|
|
227
|
+
|
|
228
|
+
# Get all collections
|
|
229
|
+
collections = zot.collections(**params)
|
|
230
|
+
|
|
231
|
+
if not collections:
|
|
232
|
+
click.echo(json.dumps([]))
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# Build a mapping of collection keys to names for parent lookup
|
|
236
|
+
collection_map = {}
|
|
237
|
+
for collection in collections:
|
|
238
|
+
data = collection.get("data", {})
|
|
239
|
+
key = data.get("key", "")
|
|
240
|
+
name = data.get("name", "")
|
|
241
|
+
if key:
|
|
242
|
+
collection_map[key] = name if name else None
|
|
243
|
+
|
|
244
|
+
# Build JSON output
|
|
245
|
+
output = []
|
|
246
|
+
for collection in collections:
|
|
247
|
+
data = collection.get("data", {})
|
|
248
|
+
meta = collection.get("meta", {})
|
|
249
|
+
|
|
250
|
+
name = data.get("name", "")
|
|
251
|
+
key = data.get("key", "")
|
|
252
|
+
num_items = meta.get("numItems", 0)
|
|
253
|
+
parent_collection = data.get("parentCollection", "")
|
|
254
|
+
|
|
255
|
+
collection_obj = {
|
|
256
|
+
"id": key,
|
|
257
|
+
"name": name if name else None,
|
|
258
|
+
"items": num_items,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Add parent information if it exists
|
|
262
|
+
if parent_collection:
|
|
263
|
+
parent_name = collection_map.get(parent_collection)
|
|
264
|
+
collection_obj["parent"] = {
|
|
265
|
+
"id": parent_collection,
|
|
266
|
+
"name": parent_name,
|
|
267
|
+
}
|
|
268
|
+
else:
|
|
269
|
+
collection_obj["parent"] = None
|
|
270
|
+
|
|
271
|
+
output.append(collection_obj)
|
|
272
|
+
|
|
273
|
+
# Output as JSON
|
|
274
|
+
click.echo(json.dumps(output, indent=2))
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@main.command()
|
|
282
|
+
@click.pass_context
|
|
283
|
+
def itemtypes(ctx):
|
|
284
|
+
"""List all valid item types.
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
pyzotero itemtypes
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
292
|
+
zot = _get_zotero_client(locale)
|
|
293
|
+
|
|
294
|
+
# Get all item types
|
|
295
|
+
item_types = zot.item_types()
|
|
296
|
+
|
|
297
|
+
if not item_types:
|
|
298
|
+
click.echo(json.dumps([]))
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Output as JSON array
|
|
302
|
+
click.echo(json.dumps(item_types, indent=2))
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
306
|
+
sys.exit(1)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
main()
|
pyzotero/zotero.py
CHANGED
|
@@ -43,24 +43,21 @@ from .filetransport import Client as File_Client
|
|
|
43
43
|
# Avoid hanging the application if there's no server response
|
|
44
44
|
timeout = 30
|
|
45
45
|
|
|
46
|
-
NOT_MODIFIED = 304
|
|
47
46
|
ONE_HOUR = 3600
|
|
48
47
|
DEFAULT_NUM_ITEMS = 50
|
|
49
48
|
DEFAULT_ITEM_LIMIT = 100
|
|
50
|
-
TOO_MANY_REQUESTS = 429
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
def build_url(base_url, path, args_dict=None):
|
|
54
52
|
"""Build a valid URL so we don't have to worry about string concatenation errors and
|
|
55
53
|
leading / trailing slashes etc.
|
|
56
|
-
Returns a list in the structure of urlparse.ParseResult
|
|
57
54
|
"""
|
|
58
55
|
base_url = base_url.removesuffix("/")
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
parsed = urlparse(base_url)
|
|
57
|
+
new_path = str(PurePosixPath(parsed.path) / path.removeprefix("/"))
|
|
61
58
|
if args_dict:
|
|
62
|
-
|
|
63
|
-
return urlunparse(
|
|
59
|
+
return urlunparse(parsed._replace(path=new_path, query=urlencode(args_dict)))
|
|
60
|
+
return urlunparse(parsed._replace(path=new_path))
|
|
64
61
|
|
|
65
62
|
|
|
66
63
|
def merge_params(url, params):
|
|
@@ -451,8 +448,6 @@ class Zotero:
|
|
|
451
448
|
Returns a JSON document
|
|
452
449
|
"""
|
|
453
450
|
full_url = build_url(self.endpoint, request)
|
|
454
|
-
# The API doesn't return this any more, so we have to cheat
|
|
455
|
-
self.self_link = request
|
|
456
451
|
# ensure that we wait if there's an active backoff
|
|
457
452
|
self._check_backoff()
|
|
458
453
|
# don't set locale if the url already contains it
|
|
@@ -485,6 +480,8 @@ class Zotero:
|
|
|
485
480
|
timeout=timeout,
|
|
486
481
|
)
|
|
487
482
|
self.request.encoding = "utf-8"
|
|
483
|
+
# The API doesn't return this any more, so we have to cheat
|
|
484
|
+
self.self_link = self.request.url
|
|
488
485
|
except httpx.UnsupportedProtocol:
|
|
489
486
|
# File URI handler logic
|
|
490
487
|
fc = File_Client()
|
|
@@ -515,18 +512,15 @@ class Zotero:
|
|
|
515
512
|
try:
|
|
516
513
|
for key, value in self.request.links.items():
|
|
517
514
|
parsed = urlparse(value["url"])
|
|
518
|
-
fragment =
|
|
515
|
+
fragment = urlunparse(("", "", parsed.path, "", parsed.query, ""))
|
|
519
516
|
extracted[key] = fragment
|
|
520
517
|
# add a 'self' link
|
|
521
|
-
parsed =
|
|
522
|
-
# strip 'format' query parameter
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
)
|
|
526
|
-
# rebuild url fragment
|
|
527
|
-
# this is a death march
|
|
518
|
+
parsed = urlparse(str(self.self_link))
|
|
519
|
+
# strip 'format' query parameter and rebuild query string
|
|
520
|
+
query_params = [(k, v) for k, v in parse_qsl(parsed.query) if k != "format"]
|
|
521
|
+
# rebuild url fragment with just path and query (consistent with other links)
|
|
528
522
|
extracted["self"] = urlunparse(
|
|
529
|
-
|
|
523
|
+
("", "", parsed.path, "", urlencode(query_params), "")
|
|
530
524
|
)
|
|
531
525
|
except KeyError:
|
|
532
526
|
# No links present, because it's a single item
|
|
@@ -572,7 +566,7 @@ class Zotero:
|
|
|
572
566
|
)
|
|
573
567
|
if backoff:
|
|
574
568
|
self._set_backoff(backoff)
|
|
575
|
-
return req.status_code == NOT_MODIFIED
|
|
569
|
+
return req.status_code == httpx.codes.NOT_MODIFIED
|
|
576
570
|
# Still plenty of life left in't
|
|
577
571
|
return False
|
|
578
572
|
|
|
@@ -1647,7 +1641,7 @@ def error_handler(zot, req, exc=None):
|
|
|
1647
1641
|
|
|
1648
1642
|
if error_codes.get(req.status_code):
|
|
1649
1643
|
# check to see whether its 429
|
|
1650
|
-
if req.status_code == TOO_MANY_REQUESTS:
|
|
1644
|
+
if req.status_code == httpx.codes.TOO_MANY_REQUESTS:
|
|
1651
1645
|
# try to get backoff or delay duration
|
|
1652
1646
|
delay = req.headers.get("backoff") or req.headers.get("retry-after")
|
|
1653
1647
|
if not delay:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyzotero
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Python wrapper for the Zotero API
|
|
5
5
|
Keywords: Zotero,DH
|
|
6
6
|
Author: Stephan Hügel
|
|
@@ -57,10 +57,12 @@ Requires-Dist: feedparser>=6.0.12
|
|
|
57
57
|
Requires-Dist: bibtexparser>=1.4.3,<2.0.0
|
|
58
58
|
Requires-Dist: httpx>=0.28.1
|
|
59
59
|
Requires-Dist: whenever>=0.8.8
|
|
60
|
+
Requires-Dist: click>=8.0.0 ; extra == 'cli'
|
|
60
61
|
Requires-Python: >=3.9
|
|
61
62
|
Project-URL: Repository, https://github.com/urschrei/pyzotero
|
|
62
63
|
Project-URL: Tracker, https://github.com/urschrei/pyzotero/issues
|
|
63
64
|
Project-URL: documentation, https://pyzotero.readthedocs.org
|
|
65
|
+
Provides-Extra: cli
|
|
64
66
|
Description-Content-Type: text/markdown
|
|
65
67
|
|
|
66
68
|
[](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)
|
|
@@ -93,11 +95,74 @@ for item in items:
|
|
|
93
95
|
|
|
94
96
|
Full documentation of available Pyzotero methods, code examples, and sample output is available on [Read The Docs][3].
|
|
95
97
|
|
|
98
|
+
# Command-Line Interface
|
|
99
|
+
|
|
100
|
+
Pyzotero includes an optional command-line interface for searching and querying your local Zotero library. The CLI must be installed separately (see [Installation](#optional-command-line-interface)).
|
|
101
|
+
|
|
102
|
+
## Basic Usage
|
|
103
|
+
|
|
104
|
+
The CLI connects to your local Zotero installation and allows you to search your library, list collections, and view item types:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Search for top-level items
|
|
108
|
+
pyzotero search -q "machine learning"
|
|
109
|
+
|
|
110
|
+
# Search with full-text mode
|
|
111
|
+
pyzotero search -q "climate change" --fulltext
|
|
112
|
+
|
|
113
|
+
# Filter by item type
|
|
114
|
+
pyzotero search -q "methodology" --itemtype book --itemtype journalArticle
|
|
115
|
+
|
|
116
|
+
# Search for top-level items within a collection
|
|
117
|
+
pyzotero search --collection ABC123 -q "test"
|
|
118
|
+
|
|
119
|
+
# Output as JSON for machine processing
|
|
120
|
+
pyzotero search -q "climate" --json
|
|
121
|
+
|
|
122
|
+
# List all collections
|
|
123
|
+
pyzotero listcollections
|
|
124
|
+
|
|
125
|
+
# List available item types
|
|
126
|
+
pyzotero itemtypes
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Output Format
|
|
130
|
+
|
|
131
|
+
By default, the CLI outputs human-readable text with a subset of metadata including:
|
|
132
|
+
- Title, authors, date, publication
|
|
133
|
+
- Volume, issue, DOI, URL
|
|
134
|
+
- PDF attachments (with local file paths)
|
|
135
|
+
|
|
136
|
+
Use the `--json` flag to output structured JSON.
|
|
137
|
+
|
|
96
138
|
# Installation
|
|
97
139
|
|
|
98
140
|
* Using [uv][11]: `uv add pyzotero`
|
|
99
141
|
* Using [pip][10]: `pip install pyzotero`
|
|
100
142
|
* Using Anaconda:`conda install conda-forge::pyzotero`
|
|
143
|
+
|
|
144
|
+
## Optional: Command-Line Interface
|
|
145
|
+
|
|
146
|
+
Pyzotero includes an optional command-line interface for searching and querying your local Zotero library.
|
|
147
|
+
|
|
148
|
+
### Installing the CLI
|
|
149
|
+
|
|
150
|
+
To install Pyzotero with the CLI:
|
|
151
|
+
|
|
152
|
+
* Using [uv][11]: `uv add "pyzotero[cli]"`
|
|
153
|
+
* Using [pip][10]: `pip install "pyzotero[cli]"`
|
|
154
|
+
|
|
155
|
+
### Using the CLI without installing
|
|
156
|
+
|
|
157
|
+
If you just want to use the CLI without permanently installing Pyzotero, you can run it directly:
|
|
158
|
+
|
|
159
|
+
* Using [uvx][11]: `uvx --from "pyzotero[cli]" pyzotero search -q "your query"`
|
|
160
|
+
* Using [pipx][10]: `pipx run --spec "pyzotero[cli]" pyzotero search -q "your query"`
|
|
161
|
+
|
|
162
|
+
See the [Command-Line Interface](#command-line-interface) section below for usage details.
|
|
163
|
+
|
|
164
|
+
## Installing from Source
|
|
165
|
+
|
|
101
166
|
* From a local clone, if you wish to install Pyzotero from a specific branch:
|
|
102
167
|
|
|
103
168
|
Example:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
|
|
2
|
+
pyzotero/cli.py,sha256=H7R9Nx5OEyrET1S19kQrC2g_rDq_7_RE9iCe_dKBEwY,9126
|
|
3
|
+
pyzotero/filetransport.py,sha256=umLik1LLmrpgaNmyjvtBoqqcaMgIq79PYsTvN5vG-gY,5530
|
|
4
|
+
pyzotero/zotero.py,sha256=4qb7jLl1lNkDv3WpEPLW2L0SbleTtGYlQ6Rloz-hmN0,76497
|
|
5
|
+
pyzotero/zotero_errors.py,sha256=6obx9-pBO0z1bxt33vuzDluELvA5kSLCsfc-uGc3KNw,2660
|
|
6
|
+
pyzotero-1.7.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
7
|
+
pyzotero-1.7.0.dist-info/entry_points.txt,sha256=MzN7IMRj_oPNmDCsseYFPum3bHWE1gFxywhlbFbcn2k,48
|
|
8
|
+
pyzotero-1.7.0.dist-info/METADATA,sha256=P26XM2Jb11lOpAw4McrEZFwNpwvr9jyMfvC_2v6MVr8,9233
|
|
9
|
+
pyzotero-1.7.0.dist-info/RECORD,,
|
pyzotero-1.6.16.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pyzotero/__init__.py,sha256=5QI4Jou9L-YJAf_oN9TgRXVKgt_Unc39oADo2Ch8bLI,243
|
|
2
|
-
pyzotero/filetransport.py,sha256=umLik1LLmrpgaNmyjvtBoqqcaMgIq79PYsTvN5vG-gY,5530
|
|
3
|
-
pyzotero/zotero.py,sha256=wDKLPjNt-Eo1XUnLlgAscNKZkDI6J60_nVbplXsGk8A,76441
|
|
4
|
-
pyzotero/zotero_errors.py,sha256=6obx9-pBO0z1bxt33vuzDluELvA5kSLCsfc-uGc3KNw,2660
|
|
5
|
-
pyzotero-1.6.16.dist-info/WHEEL,sha256=pFCy50wRV2h7SjJ35YOsQUupaV45rMdgpNIvnXbG5bE,79
|
|
6
|
-
pyzotero-1.6.16.dist-info/METADATA,sha256=oFB4Mcb6MtVf9eyd2cOz0LCwpzrT-CU6uOIzwDvh3jU,7292
|
|
7
|
-
pyzotero-1.6.16.dist-info/RECORD,,
|