zotero-rb 0.1.3 → 0.1.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +1 -1
- data/lib/zotero/client.rb +126 -51
- data/lib/zotero/fields.rb +13 -3
- data/lib/zotero/{library_file_operations.rb → file_attachments.rb} +25 -5
- data/lib/zotero/file_upload.rb +5 -4
- data/lib/zotero/fulltext.rb +20 -3
- data/lib/zotero/http_config.rb +23 -0
- data/lib/zotero/http_connection.rb +48 -0
- data/lib/zotero/http_errors.rb +12 -8
- data/lib/zotero/item_types.rb +24 -4
- data/lib/zotero/library.rb +81 -16
- data/lib/zotero/network_errors.rb +50 -0
- data/lib/zotero/syncing.rb +25 -4
- data/lib/zotero/version.rb +1 -1
- metadata +7 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6480cd849dc46519c7a122f454e93f34befae86b340703dedc69781c59cb9a37
|
|
4
|
+
data.tar.gz: c9890904401b2589eef07693b5e42fb798bbf81779fc2449e941cd6a1a33110b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c15301b1d99903ee690fca6dc77de020122fdf82ea46fe156238e16ef792b44e0ab6847548ff6bf9a563264a255719fb1caada748adbf9d8c0e1b0f35aaa4a7
|
|
7
|
+
data.tar.gz: a25a0ad4d3e46e8577380c653fc25f472232c5d631451135f5c3acba468d8970f9cf21f4789b9ad691a1f2b2a5e4ca8651d06cef1f7e7081d42f619da404b2f5
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.5](https://github.com/andrewhwaller/zotero-rb/compare/v0.1.4...v0.1.5) (2025-09-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* correct test expectations for refactored parameter signature ([eaad0bb](https://github.com/andrewhwaller/zotero-rb/commit/eaad0bbd0ce87cbe640b4ca7190759170ac11494))
|
|
14
|
+
* Correct test method calls to match API signatures ([76dd63e](https://github.com/andrewhwaller/zotero-rb/commit/76dd63e4d6e36c18629c377d7084efc8c6159560))
|
|
15
|
+
|
|
16
|
+
## [1.0.0](https://github.com/andrewhwaller/zotero-rb/compare/v0.1.2...v1.0.0) (2025-09-04)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### ⚠ BREAKING CHANGES
|
|
20
|
+
|
|
21
|
+
* Internal HTTP implementation migrated from HTTParty to Net::HTTP
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* replace HTTParty with Net::HTTP to eliminate dependencies ([f1af7cc](https://github.com/andrewhwaller/zotero-rb/commit/f1af7ccd27cf401bd963062763b701f2b32ea923))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* avoid Digest mocking to resolve CI RSpec environment issues ([94adc4a](https://github.com/andrewhwaller/zotero-rb/commit/94adc4a7edd6fd8362c9794f1b1826fd8dda5ec9))
|
|
31
|
+
* move digest require to top level for test compatibility ([c88a64b](https://github.com/andrewhwaller/zotero-rb/commit/c88a64b8972018b9e01682cd9f405f0d3b5f4fec))
|
|
32
|
+
|
|
8
33
|
## [0.1.2](https://github.com/andrewhwaller/zotero-rb/compare/v0.1.1...v0.1.2) (2025-09-04)
|
|
9
34
|
|
|
10
35
|
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
A comprehensive Ruby client for the [Zotero Web API v3](https://www.zotero.org/support/dev/web_api/v3/start).
|
|
7
7
|
|
|
8
|
-
NOTE: This gem is experimental and has not been fully tested with real data. So far, the gem has been set up to cover Zotero's web API documentation as much as possible, but testing is still ongoing. Do not use this gem for production applications without exercising due caution.
|
|
8
|
+
NOTE: This gem is experimental and has not been fully tested with real data. So far, the gem has been set up to cover Zotero's web API documentation as much as possible, but testing is still ongoing. Do not use this gem for production applications without exercising due caution. Having said that, if you come across something that doesn't work, open up an issue or even a PR and I'd be happy to get a fix going.
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
data/lib/zotero/client.rb
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "cgi"
|
|
4
7
|
require_relative "item_types"
|
|
5
8
|
require_relative "fields"
|
|
6
9
|
require_relative "file_upload"
|
|
7
10
|
require_relative "http_errors"
|
|
8
11
|
require_relative "syncing"
|
|
12
|
+
require_relative "http_config"
|
|
13
|
+
require_relative "http_connection"
|
|
14
|
+
require_relative "network_errors"
|
|
9
15
|
|
|
10
16
|
module Zotero
|
|
11
17
|
# The main HTTP client for interacting with the Zotero Web API v3.
|
|
@@ -15,65 +21,25 @@ module Zotero
|
|
|
15
21
|
# client = Zotero::Client.new(api_key: 'your-api-key-here')
|
|
16
22
|
# library = client.user_library(12345)
|
|
17
23
|
#
|
|
24
|
+
# rubocop:disable Metrics/ClassLength
|
|
18
25
|
class Client
|
|
19
|
-
include HTTParty
|
|
20
26
|
include ItemTypes
|
|
21
27
|
include Fields
|
|
22
28
|
include FileUpload
|
|
23
29
|
include HTTPErrors
|
|
24
30
|
include Syncing
|
|
31
|
+
include NetworkErrors
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
BASE_URI = "https://api.zotero.org"
|
|
27
34
|
|
|
28
35
|
# Initialize a new Zotero API client.
|
|
29
36
|
#
|
|
30
37
|
# @param api_key [String] Your Zotero API key from https://www.zotero.org/settings/keys
|
|
38
|
+
# @raise [ArgumentError] if api_key is nil or empty
|
|
31
39
|
def initialize(api_key:)
|
|
32
40
|
@api_key = api_key
|
|
33
41
|
end
|
|
34
42
|
|
|
35
|
-
def get(path, params: {})
|
|
36
|
-
response = self.class.get(path,
|
|
37
|
-
headers: auth_headers.merge(default_headers),
|
|
38
|
-
query: params)
|
|
39
|
-
handle_response(response, params[:format])
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def post(path, data:, version: nil, write_token: nil, params: {})
|
|
43
|
-
headers = build_write_headers(version: version, write_token: write_token)
|
|
44
|
-
response = self.class.post(path,
|
|
45
|
-
headers: headers,
|
|
46
|
-
body: data,
|
|
47
|
-
query: params)
|
|
48
|
-
handle_write_response(response)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def patch(path, data:, version: nil, params: {})
|
|
52
|
-
headers = build_write_headers(version: version)
|
|
53
|
-
response = self.class.patch(path,
|
|
54
|
-
headers: headers,
|
|
55
|
-
body: data,
|
|
56
|
-
query: params)
|
|
57
|
-
handle_write_response(response)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def put(path, data:, version: nil, params: {})
|
|
61
|
-
headers = build_write_headers(version: version)
|
|
62
|
-
response = self.class.put(path,
|
|
63
|
-
headers: headers,
|
|
64
|
-
body: data,
|
|
65
|
-
query: params)
|
|
66
|
-
handle_write_response(response)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def delete(path, version: nil, params: {})
|
|
70
|
-
headers = build_write_headers(version: version)
|
|
71
|
-
response = self.class.delete(path,
|
|
72
|
-
headers: headers,
|
|
73
|
-
query: params)
|
|
74
|
-
handle_write_response(response)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
43
|
# Get a Library instance for a specific user.
|
|
78
44
|
#
|
|
79
45
|
# @param user_id [Integer, String] The Zotero user ID
|
|
@@ -90,9 +56,46 @@ module Zotero
|
|
|
90
56
|
Library.new(client: self, type: :group, id: group_id)
|
|
91
57
|
end
|
|
92
58
|
|
|
93
|
-
|
|
59
|
+
# Make a GET request to the Zotero API.
|
|
60
|
+
# This is the main public interface for read operations.
|
|
61
|
+
#
|
|
62
|
+
# @param path [String] The API endpoint path
|
|
63
|
+
# @param params [Hash] Query parameters for the request
|
|
64
|
+
# @return [Array, Hash] The parsed response data
|
|
65
|
+
def make_get_request(path, params: {})
|
|
66
|
+
headers = auth_headers.merge(default_headers)
|
|
67
|
+
response = http_request(:get, path, headers: headers, params: params)
|
|
68
|
+
handle_response(response, params[:format])
|
|
69
|
+
end
|
|
94
70
|
|
|
95
|
-
|
|
71
|
+
# Make a write request (POST, PATCH, PUT, DELETE) to the Zotero API.
|
|
72
|
+
# This is the main public interface for write operations.
|
|
73
|
+
#
|
|
74
|
+
# @param method [Symbol] The HTTP method (:post, :patch, :put, :delete)
|
|
75
|
+
# @param path [String] The API endpoint path
|
|
76
|
+
# @param data [Hash, Array] Optional request body data
|
|
77
|
+
# @param options [Hash] Write options (version: Integer, write_token: String)
|
|
78
|
+
# @param params [Hash] Query parameters for the request
|
|
79
|
+
# @return [Hash, Boolean] The parsed response data or success status
|
|
80
|
+
def make_write_request(method, path, data: nil, options: {}, params: {})
|
|
81
|
+
headers = build_write_headers(version: options[:version], write_token: options[:write_token])
|
|
82
|
+
response = http_request(method, path, headers: headers, body: data, params: params)
|
|
83
|
+
handle_write_response(response)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
protected
|
|
87
|
+
|
|
88
|
+
def http_request(method, path, **options)
|
|
89
|
+
request_options = build_request_options(options)
|
|
90
|
+
uri = build_uri(path, request_options[:params])
|
|
91
|
+
|
|
92
|
+
handle_network_errors do
|
|
93
|
+
connection = HTTPConnection.new(uri)
|
|
94
|
+
request = build_request(method, uri, request_options[:headers], request_options[:body], request_options)
|
|
95
|
+
|
|
96
|
+
connection.request(request)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
96
99
|
|
|
97
100
|
def auth_headers
|
|
98
101
|
{ "Zotero-API-Key" => api_key }
|
|
@@ -111,15 +114,15 @@ module Zotero
|
|
|
111
114
|
end
|
|
112
115
|
|
|
113
116
|
def handle_response(response, format = nil)
|
|
114
|
-
return parse_response_body(response, format) if response.code.between?(200, 299)
|
|
117
|
+
return parse_response_body(response, format) if response.code.to_i.between?(200, 299)
|
|
115
118
|
|
|
116
119
|
raise_error_for_status(response)
|
|
117
120
|
end
|
|
118
121
|
|
|
119
122
|
def handle_write_response(response)
|
|
120
|
-
case response.code
|
|
123
|
+
case response.code.to_i
|
|
121
124
|
when 200
|
|
122
|
-
response
|
|
125
|
+
parse_json_response(response)
|
|
123
126
|
when 204
|
|
124
127
|
true
|
|
125
128
|
else
|
|
@@ -127,13 +130,85 @@ module Zotero
|
|
|
127
130
|
end
|
|
128
131
|
end
|
|
129
132
|
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
attr_reader :api_key
|
|
136
|
+
|
|
137
|
+
def build_request_options(options)
|
|
138
|
+
{
|
|
139
|
+
headers: options[:headers] || {},
|
|
140
|
+
body: options[:body],
|
|
141
|
+
params: options[:params] || {},
|
|
142
|
+
multipart: options[:multipart],
|
|
143
|
+
format: options[:format]
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def build_uri(path, params = {})
|
|
148
|
+
base = path.start_with?("http") ? path : "#{BASE_URI}#{path}"
|
|
149
|
+
uri = URI(base)
|
|
150
|
+
|
|
151
|
+
unless params.empty?
|
|
152
|
+
query_params = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
|
|
153
|
+
uri.query = uri.query ? "#{uri.query}&#{query_params}" : query_params
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
uri
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_request(method, uri, headers, body, request_options)
|
|
160
|
+
request = create_request(method, uri)
|
|
161
|
+
set_headers(request, headers)
|
|
162
|
+
set_request_body(request, method, body, headers, request_options) if body
|
|
163
|
+
request
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def create_request(method, uri)
|
|
167
|
+
request_class = case method
|
|
168
|
+
when :get then Net::HTTP::Get
|
|
169
|
+
when :post then Net::HTTP::Post
|
|
170
|
+
when :put then Net::HTTP::Put
|
|
171
|
+
when :patch then Net::HTTP::Patch
|
|
172
|
+
when :delete then Net::HTTP::Delete
|
|
173
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
request_class.new(uri)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def set_headers(request, headers)
|
|
180
|
+
headers.each { |key, value| request[key] = value }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def set_request_body(request, method, body, headers, request_options)
|
|
184
|
+
return unless %i[post put patch].include?(method)
|
|
185
|
+
|
|
186
|
+
if request_options[:multipart]
|
|
187
|
+
request.set_form(body, "multipart/form-data")
|
|
188
|
+
elsif headers["Content-Type"] == "application/x-www-form-urlencoded"
|
|
189
|
+
request.set_form_data(body)
|
|
190
|
+
else
|
|
191
|
+
request.body = body.is_a?(String) ? body : JSON.generate(body)
|
|
192
|
+
request["Content-Type"] = "application/json" unless headers["Content-Type"]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
130
196
|
def parse_response_body(response, format)
|
|
131
197
|
case format&.to_s
|
|
132
198
|
when "json", nil
|
|
133
|
-
response
|
|
199
|
+
parse_json_response(response)
|
|
134
200
|
else
|
|
135
201
|
response.body
|
|
136
202
|
end
|
|
137
203
|
end
|
|
204
|
+
|
|
205
|
+
def parse_json_response(response)
|
|
206
|
+
return nil if response.body.nil? || response.body.empty?
|
|
207
|
+
|
|
208
|
+
JSON.parse(response.body)
|
|
209
|
+
rescue JSON::ParserError
|
|
210
|
+
response.body
|
|
211
|
+
end
|
|
138
212
|
end
|
|
213
|
+
# rubocop:enable Metrics/ClassLength
|
|
139
214
|
end
|
data/lib/zotero/fields.rb
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zotero
|
|
4
|
-
# Field discovery methods
|
|
4
|
+
# Field discovery methods for Zotero items and creators
|
|
5
5
|
module Fields
|
|
6
|
+
# Get all available item fields.
|
|
7
|
+
#
|
|
8
|
+
# @param locale [String] Optional locale for localized field names (e.g. 'en-US', 'fr-FR')
|
|
9
|
+
# @return [Array<Hash>] Array of field definitions with field names and localized labels
|
|
6
10
|
def item_fields(locale: nil)
|
|
7
|
-
|
|
11
|
+
params = build_locale_params(locale)
|
|
12
|
+
make_get_request("/itemFields", params: params)
|
|
8
13
|
end
|
|
9
14
|
|
|
15
|
+
# Get all available creator fields.
|
|
16
|
+
#
|
|
17
|
+
# @param locale [String] Optional locale for localized field names (e.g. 'en-US', 'fr-FR')
|
|
18
|
+
# @return [Array<Hash>] Array of creator field definitions with field names and localized labels
|
|
10
19
|
def creator_fields(locale: nil)
|
|
11
|
-
|
|
20
|
+
params = build_locale_params(locale)
|
|
21
|
+
make_get_request("/creatorFields", params: params)
|
|
12
22
|
end
|
|
13
23
|
|
|
14
24
|
private
|
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module Zotero
|
|
4
|
-
# File
|
|
5
|
-
module
|
|
6
|
+
# File attachment operations for library items
|
|
7
|
+
module FileAttachments
|
|
8
|
+
# Create a new attachment item in the library.
|
|
9
|
+
#
|
|
10
|
+
# @param attachment_data [Hash] The attachment data including itemType, contentType, etc.
|
|
11
|
+
# @param version [Integer] Optional version for conditional requests
|
|
12
|
+
# @param write_token [String] Optional write token for batch operations
|
|
13
|
+
# @return [Hash] The API response with created attachment
|
|
6
14
|
def create_attachment(attachment_data, version: nil, write_token: nil)
|
|
7
15
|
create_single("items", attachment_data, version: version, write_token: write_token)
|
|
8
16
|
end
|
|
9
17
|
|
|
18
|
+
# Get file information for an attachment item.
|
|
19
|
+
#
|
|
20
|
+
# @param item_key [String] The attachment item key
|
|
21
|
+
# @return [Hash] File information including filename, md5, mtime
|
|
10
22
|
def get_file_info(item_key)
|
|
11
|
-
@client.
|
|
23
|
+
@client.make_get_request("#{@base_path}/items/#{item_key}/file")
|
|
12
24
|
end
|
|
13
25
|
|
|
26
|
+
# Upload a file to an attachment item.
|
|
27
|
+
#
|
|
28
|
+
# @param item_key [String] The attachment item key
|
|
29
|
+
# @param file_path [String] Local path to the file to upload
|
|
30
|
+
# @return [Boolean] Success status
|
|
14
31
|
def upload_file(item_key, file_path)
|
|
15
32
|
perform_file_upload(item_key, file_path, existing_file: false)
|
|
16
33
|
end
|
|
17
34
|
|
|
35
|
+
# Update the file content of an existing attachment.
|
|
36
|
+
#
|
|
37
|
+
# @param item_key [String] The attachment item key
|
|
38
|
+
# @param file_path [String] Local path to the new file
|
|
39
|
+
# @return [Boolean] Success status
|
|
18
40
|
def update_file(item_key, file_path)
|
|
19
41
|
perform_file_upload(item_key, file_path, existing_file: true)
|
|
20
42
|
end
|
|
@@ -36,8 +58,6 @@ module Zotero
|
|
|
36
58
|
end
|
|
37
59
|
|
|
38
60
|
def extract_file_metadata(file_path)
|
|
39
|
-
require "digest"
|
|
40
|
-
|
|
41
61
|
{
|
|
42
62
|
filename: File.basename(file_path),
|
|
43
63
|
md5: Digest::MD5.file(file_path).hexdigest,
|
data/lib/zotero/file_upload.rb
CHANGED
|
@@ -9,18 +9,19 @@ module Zotero
|
|
|
9
9
|
headers["If-Match"] = if_match if if_match
|
|
10
10
|
headers["If-None-Match"] = if_none_match if if_none_match
|
|
11
11
|
|
|
12
|
-
response =
|
|
12
|
+
response = http_request(:post, path, headers: headers, body: form_data, params: params)
|
|
13
13
|
handle_response(response)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def external_post(url, multipart_data:)
|
|
17
|
-
response =
|
|
17
|
+
response = http_request(:post, url, body: multipart_data, multipart: true, format: :plain)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
code = response.code.to_i
|
|
20
|
+
case code
|
|
20
21
|
when 200..299
|
|
21
22
|
response.body
|
|
22
23
|
else
|
|
23
|
-
raise Error, "External upload failed: HTTP #{
|
|
24
|
+
raise Error, "External upload failed: HTTP #{code} - #{response.message}"
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
|
data/lib/zotero/fulltext.rb
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zotero
|
|
4
|
+
# Fulltext search and content methods
|
|
4
5
|
module Fulltext
|
|
6
|
+
# Get fulltext content that has been modified since a given version.
|
|
7
|
+
#
|
|
8
|
+
# @param since [Integer] Version number to get changes since
|
|
9
|
+
# @return [Hash] Object mapping item keys to version numbers
|
|
5
10
|
def fulltext_since(since:)
|
|
6
|
-
|
|
11
|
+
params = { since: since }
|
|
12
|
+
@client.make_get_request("#{@base_path}/fulltext", params: params)
|
|
7
13
|
end
|
|
8
14
|
|
|
15
|
+
# Get the fulltext content for a specific item.
|
|
16
|
+
#
|
|
17
|
+
# @param item_key [String] The item key to get fulltext for
|
|
18
|
+
# @return [Hash] Fulltext content data including content, indexedChars, and totalChars
|
|
9
19
|
def item_fulltext(item_key)
|
|
10
|
-
@client.
|
|
20
|
+
@client.make_get_request("#{@base_path}/items/#{item_key}/fulltext")
|
|
11
21
|
end
|
|
12
22
|
|
|
23
|
+
# Set the fulltext content for a specific item.
|
|
24
|
+
#
|
|
25
|
+
# @param item_key [String] The item key to set fulltext for
|
|
26
|
+
# @param content_data [Hash] Fulltext content data with content, indexedChars, totalChars
|
|
27
|
+
# @param version [Integer] Optional version for optimistic concurrency control
|
|
28
|
+
# @return [Boolean] Success status
|
|
13
29
|
def set_item_fulltext(item_key, content_data, version: nil)
|
|
14
|
-
@client.put
|
|
30
|
+
@client.make_write_request(:put, "#{@base_path}/items/#{item_key}/fulltext", data: content_data,
|
|
31
|
+
options: { version: version })
|
|
15
32
|
end
|
|
16
33
|
end
|
|
17
34
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zotero
|
|
4
|
+
# Configuration for HTTP requests
|
|
5
|
+
class HTTPConfig
|
|
6
|
+
attr_accessor :open_timeout, :read_timeout, :verify_ssl
|
|
7
|
+
|
|
8
|
+
def initialize(open_timeout: 30, read_timeout: 60, verify_ssl: true)
|
|
9
|
+
@open_timeout = open_timeout
|
|
10
|
+
@read_timeout = read_timeout
|
|
11
|
+
@verify_ssl = verify_ssl
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.default
|
|
15
|
+
@default ||= new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.configure
|
|
19
|
+
yield(default) if block_given?
|
|
20
|
+
default
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Zotero
|
|
7
|
+
# Manages HTTP connections with proper SSL configuration and timeouts
|
|
8
|
+
class HTTPConnection
|
|
9
|
+
DEFAULT_OPEN_TIMEOUT = 30
|
|
10
|
+
DEFAULT_READ_TIMEOUT = 60
|
|
11
|
+
|
|
12
|
+
def initialize(uri, config = nil)
|
|
13
|
+
@uri = uri
|
|
14
|
+
@config = config || HTTPConfig.default
|
|
15
|
+
@http = build_connection
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def request(net_request)
|
|
19
|
+
configure_connection unless @configured
|
|
20
|
+
@http.request(net_request)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :uri, :config, :http
|
|
26
|
+
|
|
27
|
+
def build_connection
|
|
28
|
+
Net::HTTP.new(@uri.host, @uri.port)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configure_connection
|
|
32
|
+
configure_ssl if @uri.scheme == "https"
|
|
33
|
+
configure_timeouts
|
|
34
|
+
@configured = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def configure_ssl
|
|
38
|
+
@http.use_ssl = true
|
|
39
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
40
|
+
@http.ca_file = OpenSSL::X509::DEFAULT_CERT_FILE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configure_timeouts
|
|
44
|
+
@http.open_timeout = @config.open_timeout
|
|
45
|
+
@http.read_timeout = @config.read_timeout
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/zotero/http_errors.rb
CHANGED
|
@@ -4,7 +4,8 @@ module Zotero
|
|
|
4
4
|
# HTTP error handling methods
|
|
5
5
|
module HTTPErrors
|
|
6
6
|
def raise_error_for_status(response)
|
|
7
|
-
|
|
7
|
+
code = response.code.to_i
|
|
8
|
+
case code
|
|
8
9
|
when 400..428 then raise_client_error(response)
|
|
9
10
|
when 429 then raise_rate_limit_error(response)
|
|
10
11
|
else raise_server_or_unknown_error(response)
|
|
@@ -12,10 +13,11 @@ module Zotero
|
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def raise_client_error(response)
|
|
15
|
-
|
|
16
|
+
code = response.code.to_i
|
|
17
|
+
case code
|
|
16
18
|
when 400, 413 then raise BadRequestError, "Bad request: #{response.body}"
|
|
17
19
|
when 401, 403 then raise AuthenticationError, "Authentication failed - check your API key"
|
|
18
|
-
when 404 then raise NotFoundError, "Resource not found
|
|
20
|
+
when 404 then raise NotFoundError, "Resource not found"
|
|
19
21
|
when 409 then raise ConflictError, "Conflict: #{response.body}"
|
|
20
22
|
when 412 then raise PreconditionFailedError, "Precondition failed: #{response.body}"
|
|
21
23
|
when 428 then raise PreconditionRequiredError, "Precondition required: #{response.body}"
|
|
@@ -23,8 +25,9 @@ module Zotero
|
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def raise_rate_limit_error(response)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
headers = response.to_hash.transform_values(&:first)
|
|
29
|
+
backoff = headers["backoff"]&.to_i
|
|
30
|
+
retry_after = headers["retry-after"]&.to_i
|
|
28
31
|
message = "Rate limited."
|
|
29
32
|
message += " Backoff: #{backoff}s" if backoff
|
|
30
33
|
message += " Retry after: #{retry_after}s" if retry_after
|
|
@@ -32,11 +35,12 @@ module Zotero
|
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def raise_server_or_unknown_error(response)
|
|
35
|
-
|
|
38
|
+
code = response.code.to_i
|
|
39
|
+
case code
|
|
36
40
|
when 500..599
|
|
37
|
-
raise ServerError, "Server error: HTTP #{
|
|
41
|
+
raise ServerError, "Server error: HTTP #{code} - #{response.message}"
|
|
38
42
|
else
|
|
39
|
-
raise Error, "Unexpected response: HTTP #{
|
|
43
|
+
raise Error, "Unexpected response: HTTP #{code} - #{response.message}"
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
46
|
end
|
data/lib/zotero/item_types.rb
CHANGED
|
@@ -3,22 +3,42 @@
|
|
|
3
3
|
module Zotero
|
|
4
4
|
# Item type discovery and template methods
|
|
5
5
|
module ItemTypes
|
|
6
|
+
# Get all available item types.
|
|
7
|
+
#
|
|
8
|
+
# @param locale [String] Optional locale for localized type names (e.g. 'en-US', 'fr-FR')
|
|
9
|
+
# @return [Array<Hash>] Array of item type definitions
|
|
6
10
|
def item_types(locale: nil)
|
|
7
|
-
|
|
11
|
+
params = build_locale_params(locale)
|
|
12
|
+
make_get_request("/itemTypes", params: params)
|
|
8
13
|
end
|
|
9
14
|
|
|
15
|
+
# Get all fields available for a specific item type.
|
|
16
|
+
#
|
|
17
|
+
# @param item_type [String] The item type name (e.g. 'book', 'journalArticle')
|
|
18
|
+
# @param locale [String] Optional locale for localized field names
|
|
19
|
+
# @return [Array<Hash>] Array of field definitions for the item type
|
|
10
20
|
def item_type_fields(item_type, locale: nil)
|
|
11
21
|
params = { itemType: item_type }
|
|
12
22
|
params.merge!(build_locale_params(locale))
|
|
13
|
-
|
|
23
|
+
make_get_request("/itemTypeFields", params: params)
|
|
14
24
|
end
|
|
15
25
|
|
|
26
|
+
# Get all creator types available for a specific item type.
|
|
27
|
+
#
|
|
28
|
+
# @param item_type [String] The item type name (e.g. 'book', 'journalArticle')
|
|
29
|
+
# @return [Array<Hash>] Array of creator type definitions for the item type
|
|
16
30
|
def item_type_creator_types(item_type)
|
|
17
|
-
|
|
31
|
+
params = { itemType: item_type }
|
|
32
|
+
make_get_request("/itemTypeCreatorTypes", params: params)
|
|
18
33
|
end
|
|
19
34
|
|
|
35
|
+
# Get a new item template for a specific item type.
|
|
36
|
+
#
|
|
37
|
+
# @param item_type [String] The item type name (e.g. 'book', 'journalArticle')
|
|
38
|
+
# @return [Hash] Template object with empty fields for the item type
|
|
20
39
|
def new_item_template(item_type)
|
|
21
|
-
|
|
40
|
+
params = { itemType: item_type }
|
|
41
|
+
make_get_request("/items/new", params: params)
|
|
22
42
|
end
|
|
23
43
|
|
|
24
44
|
private
|
data/lib/zotero/library.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "file_attachments"
|
|
4
4
|
require_relative "fulltext"
|
|
5
5
|
require_relative "syncing"
|
|
6
6
|
|
|
@@ -15,8 +15,7 @@ module Zotero
|
|
|
15
15
|
# collections = library.collections
|
|
16
16
|
#
|
|
17
17
|
class Library
|
|
18
|
-
|
|
19
|
-
include LibraryFileOperations
|
|
18
|
+
include FileAttachments
|
|
20
19
|
include Fulltext
|
|
21
20
|
include Syncing
|
|
22
21
|
|
|
@@ -39,7 +38,7 @@ module Zotero
|
|
|
39
38
|
# @param params [Hash] Query parameters for the request
|
|
40
39
|
# @return [Array, Hash] Collections data from the API
|
|
41
40
|
def collections(**params)
|
|
42
|
-
@client.
|
|
41
|
+
@client.make_get_request("#{@base_path}/collections", params: params)
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
# Get items in this library.
|
|
@@ -47,15 +46,23 @@ module Zotero
|
|
|
47
46
|
# @param params [Hash] Query parameters for the request
|
|
48
47
|
# @return [Array, Hash] Items data from the API
|
|
49
48
|
def items(**params)
|
|
50
|
-
@client.
|
|
49
|
+
@client.make_get_request("#{@base_path}/items", params: params)
|
|
51
50
|
end
|
|
52
51
|
|
|
52
|
+
# Get saved searches in this library.
|
|
53
|
+
#
|
|
54
|
+
# @param params [Hash] Query parameters for the request
|
|
55
|
+
# @return [Array, Hash] Saved searches data from the API
|
|
53
56
|
def searches(**params)
|
|
54
|
-
@client.
|
|
57
|
+
@client.make_get_request("#{@base_path}/searches", params: params)
|
|
55
58
|
end
|
|
56
59
|
|
|
60
|
+
# Get tags in this library.
|
|
61
|
+
#
|
|
62
|
+
# @param params [Hash] Query parameters for the request
|
|
63
|
+
# @return [Array, Hash] Tags data from the API
|
|
57
64
|
def tags(**params)
|
|
58
|
-
@client.
|
|
65
|
+
@client.make_get_request("#{@base_path}/tags", params: params)
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
# Create a new item in this library.
|
|
@@ -68,41 +75,95 @@ module Zotero
|
|
|
68
75
|
create_single("items", item_data, version: version, write_token: write_token)
|
|
69
76
|
end
|
|
70
77
|
|
|
78
|
+
# Create multiple items in this library.
|
|
79
|
+
#
|
|
80
|
+
# @param items_array [Array<Hash>] Array of item data objects
|
|
81
|
+
# @param version [Integer] Optional version for conditional requests
|
|
82
|
+
# @param write_token [String] Optional write token for batch operations
|
|
83
|
+
# @return [Hash] The API response with created items
|
|
71
84
|
def create_items(items_array, version: nil, write_token: nil)
|
|
72
85
|
create_multiple("items", items_array, version: version, write_token: write_token)
|
|
73
86
|
end
|
|
74
87
|
|
|
88
|
+
# Update an existing item in this library.
|
|
89
|
+
#
|
|
90
|
+
# @param item_key [String] The item key to update
|
|
91
|
+
# @param item_data [Hash] The updated item data
|
|
92
|
+
# @param version [Integer] Version for optimistic concurrency control
|
|
93
|
+
# @return [Hash] The API response
|
|
75
94
|
def update_item(item_key, item_data, version: nil)
|
|
76
|
-
@client.patch
|
|
95
|
+
@client.make_write_request(:patch, "#{@base_path}/items/#{item_key}", data: item_data,
|
|
96
|
+
options: { version: version })
|
|
77
97
|
end
|
|
78
98
|
|
|
99
|
+
# Delete an item from this library.
|
|
100
|
+
#
|
|
101
|
+
# @param item_key [String] The item key to delete
|
|
102
|
+
# @param version [Integer] Version for optimistic concurrency control
|
|
103
|
+
# @return [Boolean] Success status
|
|
79
104
|
def delete_item(item_key, version: nil)
|
|
80
|
-
@client.delete
|
|
105
|
+
@client.make_write_request(:delete, "#{@base_path}/items/#{item_key}", options: { version: version })
|
|
81
106
|
end
|
|
82
107
|
|
|
108
|
+
# Delete multiple items from this library.
|
|
109
|
+
#
|
|
110
|
+
# @param item_keys [Array<String>] Array of item keys to delete
|
|
111
|
+
# @param version [Integer] Version for optimistic concurrency control
|
|
112
|
+
# @return [Boolean] Success status
|
|
83
113
|
def delete_items(item_keys, version: nil)
|
|
84
|
-
@client.delete
|
|
114
|
+
@client.make_write_request(:delete, "#{@base_path}/items", options: { version: version },
|
|
115
|
+
params: { itemKey: item_keys.join(",") })
|
|
85
116
|
end
|
|
86
117
|
|
|
118
|
+
# Create a new collection in this library.
|
|
119
|
+
#
|
|
120
|
+
# @param collection_data [Hash] The collection data
|
|
121
|
+
# @param version [Integer] Optional version for conditional requests
|
|
122
|
+
# @param write_token [String] Optional write token for batch operations
|
|
123
|
+
# @return [Hash] The API response
|
|
87
124
|
def create_collection(collection_data, version: nil, write_token: nil)
|
|
88
125
|
create_single("collections", collection_data, version: version, write_token: write_token)
|
|
89
126
|
end
|
|
90
127
|
|
|
128
|
+
# Create multiple collections in this library.
|
|
129
|
+
#
|
|
130
|
+
# @param collections_array [Array<Hash>] Array of collection data objects
|
|
131
|
+
# @param version [Integer] Optional version for conditional requests
|
|
132
|
+
# @param write_token [String] Optional write token for batch operations
|
|
133
|
+
# @return [Hash] The API response with created collections
|
|
91
134
|
def create_collections(collections_array, version: nil, write_token: nil)
|
|
92
135
|
create_multiple("collections", collections_array, version: version, write_token: write_token)
|
|
93
136
|
end
|
|
94
137
|
|
|
138
|
+
# Update an existing collection in this library.
|
|
139
|
+
#
|
|
140
|
+
# @param collection_key [String] The collection key to update
|
|
141
|
+
# @param collection_data [Hash] The updated collection data
|
|
142
|
+
# @param version [Integer] Version for optimistic concurrency control
|
|
143
|
+
# @return [Hash] The API response
|
|
95
144
|
def update_collection(collection_key, collection_data, version: nil)
|
|
96
|
-
@client.patch
|
|
145
|
+
@client.make_write_request(:patch, "#{@base_path}/collections/#{collection_key}", data: collection_data,
|
|
146
|
+
options: { version: version })
|
|
97
147
|
end
|
|
98
148
|
|
|
149
|
+
# Delete a collection from this library.
|
|
150
|
+
#
|
|
151
|
+
# @param collection_key [String] The collection key to delete
|
|
152
|
+
# @param version [Integer] Version for optimistic concurrency control
|
|
153
|
+
# @return [Boolean] Success status
|
|
99
154
|
def delete_collection(collection_key, version: nil)
|
|
100
|
-
@client.delete
|
|
155
|
+
@client.make_write_request(:delete, "#{@base_path}/collections/#{collection_key}", options: { version: version })
|
|
101
156
|
end
|
|
102
157
|
|
|
158
|
+
# Delete multiple collections from this library.
|
|
159
|
+
#
|
|
160
|
+
# @param collection_keys [Array<String>] Array of collection keys to delete
|
|
161
|
+
# @param version [Integer] Version for optimistic concurrency control
|
|
162
|
+
# @return [Boolean] Success status
|
|
103
163
|
def delete_collections(collection_keys, version: nil)
|
|
104
|
-
@client.delete
|
|
105
|
-
|
|
164
|
+
@client.make_write_request(:delete, "#{@base_path}/collections",
|
|
165
|
+
options: { version: version },
|
|
166
|
+
params: { collectionKey: collection_keys.join(",") })
|
|
106
167
|
end
|
|
107
168
|
|
|
108
169
|
private
|
|
@@ -110,11 +171,15 @@ module Zotero
|
|
|
110
171
|
attr_reader :client, :type, :id, :base_path
|
|
111
172
|
|
|
112
173
|
def create_single(resource, data, version: nil, write_token: nil)
|
|
113
|
-
@client.post
|
|
174
|
+
@client.make_write_request(:post, "#{@base_path}/#{resource}",
|
|
175
|
+
data: [data],
|
|
176
|
+
options: { version: version, write_token: write_token })
|
|
114
177
|
end
|
|
115
178
|
|
|
116
179
|
def create_multiple(resource, data_array, version: nil, write_token: nil)
|
|
117
|
-
@client.post
|
|
180
|
+
@client.make_write_request(:post, "#{@base_path}/#{resource}",
|
|
181
|
+
data: data_array,
|
|
182
|
+
options: { version: version, write_token: write_token })
|
|
118
183
|
end
|
|
119
184
|
|
|
120
185
|
def validate_type(type)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Zotero
|
|
7
|
+
# Handles network errors and translates them to appropriate Zotero exceptions
|
|
8
|
+
module NetworkErrors
|
|
9
|
+
ERROR_MESSAGES = {
|
|
10
|
+
Errno::ECONNREFUSED => "Connection refused - server may be down",
|
|
11
|
+
Errno::EHOSTUNREACH => "Host unreachable - check network connectivity",
|
|
12
|
+
Errno::ENETUNREACH => "Host unreachable - check network connectivity",
|
|
13
|
+
SocketError => "DNS resolution failed - check hostname",
|
|
14
|
+
Net::OpenTimeout => "Connection timeout - server took too long to respond",
|
|
15
|
+
Net::ReadTimeout => "Read timeout - server response was too slow",
|
|
16
|
+
OpenSSL::SSL::SSLError => "SSL error - certificate verification failed",
|
|
17
|
+
Timeout::Error => "Request timeout"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def handle_network_errors
|
|
21
|
+
yield
|
|
22
|
+
rescue *network_error_classes => e
|
|
23
|
+
raise translate_network_error(e)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def network_error_classes
|
|
29
|
+
[
|
|
30
|
+
Errno::ECONNREFUSED,
|
|
31
|
+
Errno::EHOSTUNREACH,
|
|
32
|
+
Errno::ENETUNREACH,
|
|
33
|
+
SocketError,
|
|
34
|
+
Net::OpenTimeout,
|
|
35
|
+
Net::ReadTimeout,
|
|
36
|
+
OpenSSL::SSL::SSLError,
|
|
37
|
+
Timeout::Error
|
|
38
|
+
]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def translate_network_error(error)
|
|
42
|
+
message = error_message_for(error)
|
|
43
|
+
Error.new("#{message} (#{error.class})")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def error_message_for(error)
|
|
47
|
+
ERROR_MESSAGES.fetch(error.class) { "Network error: #{error.message}" }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/zotero/syncing.rb
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zotero
|
|
4
|
+
# Syncing and API key verification methods
|
|
4
5
|
module Syncing
|
|
6
|
+
# Verify that the current API key is valid.
|
|
7
|
+
#
|
|
8
|
+
# @return [Hash] API key information including userID and username
|
|
5
9
|
def verify_api_key
|
|
6
|
-
|
|
10
|
+
if @client
|
|
11
|
+
@client.make_get_request("/keys/current")
|
|
12
|
+
else
|
|
13
|
+
make_get_request("/keys/current")
|
|
14
|
+
end
|
|
7
15
|
end
|
|
8
16
|
|
|
17
|
+
# Get groups for a specific user.
|
|
18
|
+
#
|
|
19
|
+
# @param user_id [Integer, String] The user ID to get groups for
|
|
20
|
+
# @param format [String] Response format ('versions' or 'json')
|
|
21
|
+
# @return [Hash, Array] Groups data in the requested format
|
|
9
22
|
def user_groups(user_id, format: "versions")
|
|
10
|
-
|
|
11
|
-
client
|
|
23
|
+
params = { format: format }
|
|
24
|
+
if @client
|
|
25
|
+
@client.make_get_request("/users/#{user_id}/groups", params: params)
|
|
26
|
+
else
|
|
27
|
+
make_get_request("/users/#{user_id}/groups", params: params)
|
|
28
|
+
end
|
|
12
29
|
end
|
|
13
30
|
|
|
31
|
+
# Get items that have been deleted from this library.
|
|
32
|
+
#
|
|
33
|
+
# @param since [Integer] Optional version to get deletions since
|
|
34
|
+
# @return [Hash] Object with deleted collections and items arrays
|
|
14
35
|
def deleted_items(since: nil)
|
|
15
36
|
params = since ? { since: since } : {}
|
|
16
|
-
@client.
|
|
37
|
+
@client.make_get_request("#{@base_path}/deleted", params: params)
|
|
17
38
|
end
|
|
18
39
|
end
|
|
19
40
|
end
|
data/lib/zotero/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,42 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zotero-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Waller
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: csv
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: httparty
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.21.0
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.21.0
|
|
11
|
+
dependencies: []
|
|
40
12
|
description: A feature-complete Ruby client for the Zotero Web API v3, providing full
|
|
41
13
|
CRUD operations for items, collections, tags, and searches, plus file uploads, fulltext
|
|
42
14
|
content access, and library synchronization. Built with a modular architecture and
|
|
@@ -60,12 +32,15 @@ files:
|
|
|
60
32
|
- lib/zotero/client.rb
|
|
61
33
|
- lib/zotero/error.rb
|
|
62
34
|
- lib/zotero/fields.rb
|
|
35
|
+
- lib/zotero/file_attachments.rb
|
|
63
36
|
- lib/zotero/file_upload.rb
|
|
64
37
|
- lib/zotero/fulltext.rb
|
|
38
|
+
- lib/zotero/http_config.rb
|
|
39
|
+
- lib/zotero/http_connection.rb
|
|
65
40
|
- lib/zotero/http_errors.rb
|
|
66
41
|
- lib/zotero/item_types.rb
|
|
67
42
|
- lib/zotero/library.rb
|
|
68
|
-
- lib/zotero/
|
|
43
|
+
- lib/zotero/network_errors.rb
|
|
69
44
|
- lib/zotero/syncing.rb
|
|
70
45
|
- lib/zotero/version.rb
|
|
71
46
|
- sig/zotero/rb.rbs
|
|
@@ -95,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
95
70
|
- !ruby/object:Gem::Version
|
|
96
71
|
version: '0'
|
|
97
72
|
requirements: []
|
|
98
|
-
rubygems_version: 3.
|
|
73
|
+
rubygems_version: 3.7.1
|
|
99
74
|
specification_version: 4
|
|
100
75
|
summary: A comprehensive Ruby client for the Zotero Web API v3
|
|
101
76
|
test_files: []
|