pyzotero 1.6.17__tar.gz → 1.7.0__tar.gz

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.
Files changed (34) hide show
  1. {pyzotero-1.6.17 → pyzotero-1.7.0}/PKG-INFO +66 -1
  2. {pyzotero-1.6.17 → pyzotero-1.7.0}/README.md +63 -0
  3. {pyzotero-1.6.17 → pyzotero-1.7.0}/doc/index.rst +82 -0
  4. {pyzotero-1.6.17 → pyzotero-1.7.0}/pyproject.toml +7 -1
  5. pyzotero-1.7.0/src/pyzotero/cli.py +310 -0
  6. {pyzotero-1.6.17 → pyzotero-1.7.0}/src/pyzotero/zotero.py +5 -6
  7. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/test_zotero.py +1 -1
  8. {pyzotero-1.6.17 → pyzotero-1.7.0}/LICENSE.md +0 -0
  9. {pyzotero-1.6.17 → pyzotero-1.7.0}/doc/Makefile +0 -0
  10. {pyzotero-1.6.17 → pyzotero-1.7.0}/doc/_templates/layout.html +0 -0
  11. {pyzotero-1.6.17 → pyzotero-1.7.0}/doc/cat.png +0 -0
  12. {pyzotero-1.6.17 → pyzotero-1.7.0}/doc/conf.py +0 -0
  13. {pyzotero-1.6.17 → pyzotero-1.7.0}/src/pyzotero/__init__.py +0 -0
  14. {pyzotero-1.6.17 → pyzotero-1.7.0}/src/pyzotero/filetransport.py +0 -0
  15. {pyzotero-1.6.17 → pyzotero-1.7.0}/src/pyzotero/zotero_errors.py +0 -0
  16. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/__init__.py +0 -0
  17. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/attachments_doc.json +0 -0
  18. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/citation_doc.xml +0 -0
  19. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/collection_doc.json +0 -0
  20. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/collection_tags.json +0 -0
  21. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/collection_versions.json +0 -0
  22. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/collections_doc.json +0 -0
  23. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/creation_doc.json +0 -0
  24. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/groups_doc.json +0 -0
  25. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/item_doc.json +0 -0
  26. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/item_fields.json +0 -0
  27. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/item_file.pdf +0 -0
  28. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/item_template.json +0 -0
  29. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/item_types.json +0 -0
  30. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/item_versions.json +0 -0
  31. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/items_doc.json +0 -0
  32. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/keys_doc.txt +0 -0
  33. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/api_responses/tags_doc.json +0 -0
  34. {pyzotero-1.6.17 → pyzotero-1.7.0}/tests/test_async.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyzotero
3
- Version: 1.6.17
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
  [![Supported Python versions](https://img.shields.io/pypi/pyversions/Pyzotero.svg?style=flat)](https://pypi.python.org/pypi/Pyzotero/) [![Docs](https://readthedocs.org/projects/pyzotero/badge/?version=latest)](http://pyzotero.readthedocs.org/en/latest/?badge=latest) [![PyPI Version](https://img.shields.io/pypi/v/Pyzotero.svg)](https://pypi.python.org/pypi/Pyzotero) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyzotero/badges/version.svg)](https://anaconda.org/conda-forge/pyzotero) [![Downloads](https://pepy.tech/badge/pyzotero)](https://pepy.tech/project/pyzotero)
@@ -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:
@@ -28,11 +28,74 @@ for item in items:
28
28
 
29
29
  Full documentation of available Pyzotero methods, code examples, and sample output is available on [Read The Docs][3].
30
30
 
31
+ # Command-Line Interface
32
+
33
+ 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)).
34
+
35
+ ## Basic Usage
36
+
37
+ The CLI connects to your local Zotero installation and allows you to search your library, list collections, and view item types:
38
+
39
+ ```bash
40
+ # Search for top-level items
41
+ pyzotero search -q "machine learning"
42
+
43
+ # Search with full-text mode
44
+ pyzotero search -q "climate change" --fulltext
45
+
46
+ # Filter by item type
47
+ pyzotero search -q "methodology" --itemtype book --itemtype journalArticle
48
+
49
+ # Search for top-level items within a collection
50
+ pyzotero search --collection ABC123 -q "test"
51
+
52
+ # Output as JSON for machine processing
53
+ pyzotero search -q "climate" --json
54
+
55
+ # List all collections
56
+ pyzotero listcollections
57
+
58
+ # List available item types
59
+ pyzotero itemtypes
60
+ ```
61
+
62
+ ## Output Format
63
+
64
+ By default, the CLI outputs human-readable text with a subset of metadata including:
65
+ - Title, authors, date, publication
66
+ - Volume, issue, DOI, URL
67
+ - PDF attachments (with local file paths)
68
+
69
+ Use the `--json` flag to output structured JSON.
70
+
31
71
  # Installation
32
72
 
33
73
  * Using [uv][11]: `uv add pyzotero`
34
74
  * Using [pip][10]: `pip install pyzotero`
35
75
  * Using Anaconda:`conda install conda-forge::pyzotero`
76
+
77
+ ## Optional: Command-Line Interface
78
+
79
+ Pyzotero includes an optional command-line interface for searching and querying your local Zotero library.
80
+
81
+ ### Installing the CLI
82
+
83
+ To install Pyzotero with the CLI:
84
+
85
+ * Using [uv][11]: `uv add "pyzotero[cli]"`
86
+ * Using [pip][10]: `pip install "pyzotero[cli]"`
87
+
88
+ ### Using the CLI without installing
89
+
90
+ If you just want to use the CLI without permanently installing Pyzotero, you can run it directly:
91
+
92
+ * Using [uvx][11]: `uvx --from "pyzotero[cli]" pyzotero search -q "your query"`
93
+ * Using [pipx][10]: `pipx run --spec "pyzotero[cli]" pyzotero search -q "your query"`
94
+
95
+ See the [Command-Line Interface](#command-line-interface) section below for usage details.
96
+
97
+ ## Installing from Source
98
+
36
99
  * From a local clone, if you wish to install Pyzotero from a specific branch:
37
100
 
38
101
  Example:
@@ -54,6 +54,24 @@ Using `pip <http://www.pip-installer.org/en/latest/index.html>`_: ``pip install
54
54
 
55
55
  Using `Anaconda <https://www.anaconda.com/distribution/>`_: ``conda install conda-forge::pyzotero``
56
56
 
57
+ -------------------------------
58
+ Optional: Command-Line Interface
59
+ -------------------------------
60
+
61
+ Pyzotero includes an optional command-line interface for searching and querying your local Zotero library.
62
+
63
+ To install Pyzotero with the CLI:
64
+
65
+ * Using `uv <https://docs.astral.sh/uv/>`_: ``uv add "pyzotero[cli]"``
66
+ * Using `pip <http://www.pip-installer.org/en/latest/index.html>`_: ``pip install "pyzotero[cli]"``
67
+
68
+ If you just want to use the CLI without permanently installing Pyzotero:
69
+
70
+ * Using `uvx <https://docs.astral.sh/uv/>`_: ``uvx --from "pyzotero[cli]" pyzotero search -q "your query"``
71
+ * Using `pipx <https://pipx.pypa.io/>`_: ``pipx run --spec "pyzotero[cli]" pyzotero search -q "your query"``
72
+
73
+ See :ref:`cli-usage` for usage details.
74
+
57
75
  From a local clone, if you wish to install Pyzotero from a specific branch:
58
76
 
59
77
  .. code-block:: bash
@@ -81,6 +99,70 @@ Testing requires installation of the ``dev`` dependency group (see above).
81
99
 
82
100
  Run ``pytest .`` from the top-level directory.
83
101
 
102
+ .. _cli-usage:
103
+
104
+ ==========================
105
+ Command-Line Interface Usage
106
+ ==========================
107
+
108
+ The Pyzotero CLI connects to your local Zotero installation and allows you to search your library, list collections, and view item types.
109
+
110
+ Basic Commands
111
+ --------------
112
+
113
+ Search for top-level items:
114
+
115
+ .. code-block:: bash
116
+
117
+ pyzotero search -q "machine learning"
118
+
119
+ Search with full-text mode:
120
+
121
+ .. code-block:: bash
122
+
123
+ pyzotero search -q "climate change" --fulltext
124
+
125
+ Filter by item type:
126
+
127
+ .. code-block:: bash
128
+
129
+ pyzotero search -q "methodology" --itemtype book --itemtype journalArticle
130
+
131
+ Search for top-level items within a collection:
132
+
133
+ .. code-block:: bash
134
+
135
+ pyzotero search --collection ABC123 -q "test"
136
+
137
+ Output as JSON for machine processing:
138
+
139
+ .. code-block:: bash
140
+
141
+ pyzotero search -q "climate" --json
142
+
143
+ List all collections:
144
+
145
+ .. code-block:: bash
146
+
147
+ pyzotero listcollections
148
+
149
+ List available item types:
150
+
151
+ .. code-block:: bash
152
+
153
+ pyzotero itemtypes
154
+
155
+ Output Format
156
+ -------------
157
+
158
+ By default, the CLI outputs human-readable text with all relevant metadata including:
159
+
160
+ * Title, authors, date, publication
161
+ * Volume, issue, DOI, URL
162
+ * PDF attachments (with local file paths)
163
+
164
+ Use the ``--json`` flag to output structured JSON suitable for consumption by other tools and agents.
165
+
84
166
 
85
167
  ======================
86
168
  Building Documentation
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyzotero"
3
- version = "1.6.17"
3
+ version = "1.7.0"
4
4
  description = "Python wrapper for the Zotero API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -33,6 +33,12 @@ Repository = "https://github.com/urschrei/pyzotero"
33
33
  Tracker = "https://github.com/urschrei/pyzotero/issues"
34
34
  documentation = "https://pyzotero.readthedocs.org"
35
35
 
36
+ [project.optional-dependencies]
37
+ cli = ["click>=8.0.0"]
38
+
39
+ [project.scripts]
40
+ pyzotero = "pyzotero.cli:main"
41
+
36
42
  [dependency-groups]
37
43
  dev = [
38
44
  "pytest >= 8.4.2",
@@ -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()
@@ -51,14 +51,13 @@ DEFAULT_ITEM_LIMIT = 100
51
51
  def build_url(base_url, path, args_dict=None):
52
52
  """Build a valid URL so we don't have to worry about string concatenation errors and
53
53
  leading / trailing slashes etc.
54
- Returns a list in the structure of urlparse.ParseResult
55
54
  """
56
55
  base_url = base_url.removesuffix("/")
57
- url_parts = list(urlparse(base_url))
58
- url_parts[2] += path
56
+ parsed = urlparse(base_url)
57
+ new_path = str(PurePosixPath(parsed.path) / path.removeprefix("/"))
59
58
  if args_dict:
60
- url_parts[4] = urlencode(args_dict)
61
- return urlunparse(url_parts)
59
+ return urlunparse(parsed._replace(path=new_path, query=urlencode(args_dict)))
60
+ return urlunparse(parsed._replace(path=new_path))
62
61
 
63
62
 
64
63
  def merge_params(url, params):
@@ -513,7 +512,7 @@ class Zotero:
513
512
  try:
514
513
  for key, value in self.request.links.items():
515
514
  parsed = urlparse(value["url"])
516
- fragment = f"{parsed[2]}?{parsed[4]}"
515
+ fragment = urlunparse(("", "", parsed.path, "", parsed.query, ""))
517
516
  extracted[key] = fragment
518
517
  # add a 'self' link
519
518
  parsed = urlparse(str(self.self_link))
@@ -413,7 +413,7 @@ class ZoteroTests(unittest.TestCase):
413
413
  zot.tags()
414
414
  self.assertEqual(zot.links["next"], "/users/436/items/top?limit=1&start=1")
415
415
  self.assertEqual(zot.links["last"], "/users/436/items/top?limit=1&start=2319")
416
- self.assertEqual(zot.links["alternate"], "/users/436/items/top?")
416
+ self.assertEqual(zot.links["alternate"], "/users/436/items/top")
417
417
 
418
418
  @httpretty.activate
419
419
  def testParseLinkHeadersPreservesAllParameters(self):
File without changes
File without changes
File without changes
File without changes
File without changes