zotero-rb 0.1.3 → 0.1.4
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 +17 -0
- data/lib/zotero/client.rb +161 -21
- data/lib/zotero/file_upload.rb +2 -2
- data/lib/zotero/http_config.rb +23 -0
- data/lib/zotero/http_connection.rb +48 -0
- data/lib/zotero/library_file_operations.rb +2 -2
- data/lib/zotero/network_errors.rb +50 -0
- data/lib/zotero/version.rb +1 -1
- metadata +5 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fec7634f993dfdf13ab89d23638bb4a1754a386749fc375eee188c0731be65cc
|
|
4
|
+
data.tar.gz: af1f817fe3c696dc7f480100db3a6bc6d78a8ed902dfe4624840af931c11e97d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7ee12554c2e9aff9a6e05e6929da003ff65fa09ebd5301e8eb56e44cfbf3c9048ad95578ede3945f09445a00175d38680dc95654f9408f067ca6e98dcf62a8f
|
|
7
|
+
data.tar.gz: c7351998cf6efff225f4b19dfdc6a8d14a5414fed82f1e81e8b68f9d8b20ead9f1a675fef2dfdd7902a97e38f540e6a849e9c31a8cc6846514c832a75494119e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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
|
+
## [1.0.0](https://github.com/andrewhwaller/zotero-rb/compare/v0.1.2...v1.0.0) (2025-09-04)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* Internal HTTP implementation migrated from HTTParty to Net::HTTP
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* replace HTTParty with Net::HTTP to eliminate dependencies ([f1af7cc](https://github.com/andrewhwaller/zotero-rb/commit/f1af7ccd27cf401bd963062763b701f2b32ea923))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* avoid Digest mocking to resolve CI RSpec environment issues ([94adc4a](https://github.com/andrewhwaller/zotero-rb/commit/94adc4a7edd6fd8362c9794f1b1826fd8dda5ec9))
|
|
23
|
+
* move digest require to top level for test compatibility ([c88a64b](https://github.com/andrewhwaller/zotero-rb/commit/c88a64b8972018b9e01682cd9f405f0d3b5f4fec))
|
|
24
|
+
|
|
8
25
|
## [0.1.2](https://github.com/andrewhwaller/zotero-rb/compare/v0.1.1...v0.1.2) (2025-09-04)
|
|
9
26
|
|
|
10
27
|
|
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,15 +21,16 @@ 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
|
#
|
|
@@ -33,44 +40,44 @@ module Zotero
|
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def get(path, params: {})
|
|
36
|
-
response =
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
response = http_request(:get, path,
|
|
44
|
+
headers: auth_headers.merge(default_headers),
|
|
45
|
+
params: params)
|
|
39
46
|
handle_response(response, params[:format])
|
|
40
47
|
end
|
|
41
48
|
|
|
42
49
|
def post(path, data:, version: nil, write_token: nil, params: {})
|
|
43
50
|
headers = build_write_headers(version: version, write_token: write_token)
|
|
44
|
-
response =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
response = http_request(:post, path,
|
|
52
|
+
headers: headers,
|
|
53
|
+
body: data,
|
|
54
|
+
params: params)
|
|
48
55
|
handle_write_response(response)
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
def patch(path, data:, version: nil, params: {})
|
|
52
59
|
headers = build_write_headers(version: version)
|
|
53
|
-
response =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
response = http_request(:patch, path,
|
|
61
|
+
headers: headers,
|
|
62
|
+
body: data,
|
|
63
|
+
params: params)
|
|
57
64
|
handle_write_response(response)
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
def put(path, data:, version: nil, params: {})
|
|
61
68
|
headers = build_write_headers(version: version)
|
|
62
|
-
response =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
response = http_request(:put, path,
|
|
70
|
+
headers: headers,
|
|
71
|
+
body: data,
|
|
72
|
+
params: params)
|
|
66
73
|
handle_write_response(response)
|
|
67
74
|
end
|
|
68
75
|
|
|
69
76
|
def delete(path, version: nil, params: {})
|
|
70
77
|
headers = build_write_headers(version: version)
|
|
71
|
-
response =
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
response = http_request(:delete, path,
|
|
79
|
+
headers: headers,
|
|
80
|
+
params: params)
|
|
74
81
|
handle_write_response(response)
|
|
75
82
|
end
|
|
76
83
|
|
|
@@ -90,6 +97,80 @@ module Zotero
|
|
|
90
97
|
Library.new(client: self, type: :group, id: group_id)
|
|
91
98
|
end
|
|
92
99
|
|
|
100
|
+
protected
|
|
101
|
+
|
|
102
|
+
def http_request(method, path, **options)
|
|
103
|
+
request_options = build_request_options(options)
|
|
104
|
+
uri = build_uri(path, request_options[:params])
|
|
105
|
+
|
|
106
|
+
handle_network_errors do
|
|
107
|
+
connection = HTTPConnection.new(uri)
|
|
108
|
+
request = build_request(method, uri, request_options[:headers], request_options[:body], request_options)
|
|
109
|
+
|
|
110
|
+
net_response = connection.request(request)
|
|
111
|
+
ResponseAdapter.new(net_response, uri)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def build_request_options(options)
|
|
116
|
+
{
|
|
117
|
+
headers: options[:headers] || {},
|
|
118
|
+
body: options[:body],
|
|
119
|
+
params: options[:params] || {},
|
|
120
|
+
multipart: options.dig(:options, :multipart),
|
|
121
|
+
format: options.dig(:options, :format)
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_uri(path, params = {})
|
|
126
|
+
base = path.start_with?("http") ? path : "#{BASE_URI}#{path}"
|
|
127
|
+
uri = URI(base)
|
|
128
|
+
|
|
129
|
+
unless params.empty?
|
|
130
|
+
query_params = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
|
|
131
|
+
uri.query = uri.query ? "#{uri.query}&#{query_params}" : query_params
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
uri
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def build_request(method, uri, headers, body, request_options)
|
|
138
|
+
request = create_request(method, uri)
|
|
139
|
+
set_headers(request, headers)
|
|
140
|
+
set_request_body(request, method, body, headers, request_options) if body
|
|
141
|
+
request
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def create_request(method, uri)
|
|
145
|
+
request_class = case method
|
|
146
|
+
when :get then Net::HTTP::Get
|
|
147
|
+
when :post then Net::HTTP::Post
|
|
148
|
+
when :put then Net::HTTP::Put
|
|
149
|
+
when :patch then Net::HTTP::Patch
|
|
150
|
+
when :delete then Net::HTTP::Delete
|
|
151
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
request_class.new(uri)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def set_headers(request, headers)
|
|
158
|
+
headers.each { |key, value| request[key] = value }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def set_request_body(request, method, body, headers, request_options)
|
|
162
|
+
return unless %i[post put patch].include?(method)
|
|
163
|
+
|
|
164
|
+
if request_options[:multipart]
|
|
165
|
+
request.set_form(body, "multipart/form-data")
|
|
166
|
+
elsif headers["Content-Type"] == "application/x-www-form-urlencoded"
|
|
167
|
+
request.set_form_data(body)
|
|
168
|
+
else
|
|
169
|
+
request.body = body.is_a?(String) ? body : JSON.generate(body)
|
|
170
|
+
request["Content-Type"] = "application/json" unless headers["Content-Type"]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
93
174
|
private
|
|
94
175
|
|
|
95
176
|
attr_reader :api_key
|
|
@@ -136,4 +217,63 @@ module Zotero
|
|
|
136
217
|
end
|
|
137
218
|
end
|
|
138
219
|
end
|
|
220
|
+
# rubocop:enable Metrics/ClassLength
|
|
221
|
+
|
|
222
|
+
# Adapter to provide HTTParty-compatible interface for Net::HTTP responses
|
|
223
|
+
class ResponseAdapter
|
|
224
|
+
attr_reader :net_response, :uri
|
|
225
|
+
|
|
226
|
+
def initialize(net_response, uri)
|
|
227
|
+
@net_response = net_response
|
|
228
|
+
@uri = uri
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def code
|
|
232
|
+
@net_response.code.to_i
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def parsed_response
|
|
236
|
+
@parsed_response ||= parse_body
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def body
|
|
240
|
+
@net_response.body
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def headers
|
|
244
|
+
@headers ||= @net_response.to_hash.transform_values(&:first)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def message
|
|
248
|
+
@net_response.message
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def request
|
|
252
|
+
@request ||= RequestAdapter.new(@uri)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def parse_body
|
|
258
|
+
content_type = @net_response.content_type
|
|
259
|
+
return nil if @net_response.body.nil? || @net_response.body.empty?
|
|
260
|
+
|
|
261
|
+
if content_type&.include?("application/json")
|
|
262
|
+
JSON.parse(@net_response.body)
|
|
263
|
+
else
|
|
264
|
+
@net_response.body
|
|
265
|
+
end
|
|
266
|
+
rescue JSON::ParserError
|
|
267
|
+
@net_response.body
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Adapter to provide request.path access for error handling
|
|
272
|
+
class RequestAdapter
|
|
273
|
+
attr_reader :path
|
|
274
|
+
|
|
275
|
+
def initialize(uri)
|
|
276
|
+
@path = uri.path
|
|
277
|
+
end
|
|
278
|
+
end
|
|
139
279
|
end
|
data/lib/zotero/file_upload.rb
CHANGED
|
@@ -9,12 +9,12 @@ 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, options: { multipart: true, format: :plain })
|
|
18
18
|
|
|
19
19
|
case response.code
|
|
20
20
|
when 200..299
|
|
@@ -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
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module Zotero
|
|
4
6
|
# File upload operations for library items
|
|
5
7
|
module LibraryFileOperations
|
|
@@ -36,8 +38,6 @@ module Zotero
|
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def extract_file_metadata(file_path)
|
|
39
|
-
require "digest"
|
|
40
|
-
|
|
41
41
|
{
|
|
42
42
|
filename: File.basename(file_path),
|
|
43
43
|
md5: Digest::MD5.file(file_path).hexdigest,
|
|
@@ -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/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.4
|
|
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
|
|
@@ -62,10 +34,13 @@ files:
|
|
|
62
34
|
- lib/zotero/fields.rb
|
|
63
35
|
- lib/zotero/file_upload.rb
|
|
64
36
|
- lib/zotero/fulltext.rb
|
|
37
|
+
- lib/zotero/http_config.rb
|
|
38
|
+
- lib/zotero/http_connection.rb
|
|
65
39
|
- lib/zotero/http_errors.rb
|
|
66
40
|
- lib/zotero/item_types.rb
|
|
67
41
|
- lib/zotero/library.rb
|
|
68
42
|
- lib/zotero/library_file_operations.rb
|
|
43
|
+
- lib/zotero/network_errors.rb
|
|
69
44
|
- lib/zotero/syncing.rb
|
|
70
45
|
- lib/zotero/version.rb
|
|
71
46
|
- sig/zotero/rb.rbs
|