wikiwiki 0.5.0
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 +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.ja.md +96 -0
- data/README.md +96 -0
- data/lib/wikiwiki/api.rb +222 -0
- data/lib/wikiwiki/attachment.rb +39 -0
- data/lib/wikiwiki/auth/api_key.rb +22 -0
- data/lib/wikiwiki/auth/password.rb +19 -0
- data/lib/wikiwiki/auth.rb +21 -0
- data/lib/wikiwiki/page.rb +27 -0
- data/lib/wikiwiki/rate_limiter.rb +164 -0
- data/lib/wikiwiki/sliding_window.rb +49 -0
- data/lib/wikiwiki/version.rb +7 -0
- data/lib/wikiwiki/wiki.rb +153 -0
- data/lib/wikiwiki.rb +43 -0
- data/mise.toml +6 -0
- data/rbs_collection.yaml +12 -0
- data/sig/wikiwiki/api.rbs +36 -0
- data/sig/wikiwiki/attachment.rbs +12 -0
- data/sig/wikiwiki/auth/api_key.rbs +10 -0
- data/sig/wikiwiki/auth/password.rbs +9 -0
- data/sig/wikiwiki/auth.rbs +7 -0
- data/sig/wikiwiki/page.rbs +9 -0
- data/sig/wikiwiki/rate_limiter.rbs +41 -0
- data/sig/wikiwiki/sliding_window.rbs +19 -0
- data/sig/wikiwiki/wiki.rbs +28 -0
- data/sig/wikiwiki.rbs +24 -0
- metadata +102 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wikiwiki
|
|
4
|
+
# Rate limiter with pluggable strategies
|
|
5
|
+
#
|
|
6
|
+
# @example Raise on limit
|
|
7
|
+
# limiter = Wikiwiki::RateLimiter.raise_on_limit([
|
|
8
|
+
# { window: 60, max_requests: 120 }, # 120 requests per minute
|
|
9
|
+
# { window: 3600, max_requests: 2000 } # 2000 requests per hour
|
|
10
|
+
# ])
|
|
11
|
+
# limiter.acquire! # raises RateLimitError if limit exceeded
|
|
12
|
+
#
|
|
13
|
+
# @example Wait on limit
|
|
14
|
+
# limiter = Wikiwiki::RateLimiter.wait_on_limit([
|
|
15
|
+
# { window: 60, max_requests: 120 }
|
|
16
|
+
# ])
|
|
17
|
+
# limiter.acquire! # waits automatically if limit exceeded
|
|
18
|
+
#
|
|
19
|
+
# @example No limit
|
|
20
|
+
# limiter = Wikiwiki::RateLimiter.no_limit
|
|
21
|
+
# limiter.acquire! # always succeeds immediately
|
|
22
|
+
class RateLimiter
|
|
23
|
+
# Default rate limits for Wikiwiki REST API
|
|
24
|
+
WIKIWIKI_API_LIMITS = [
|
|
25
|
+
{window: 60, max_requests: 120}, # 120 requests per minute
|
|
26
|
+
{window: 3600, max_requests: 2000} # 2000 requests per hour
|
|
27
|
+
].freeze
|
|
28
|
+
public_constant :WIKIWIKI_API_LIMITS
|
|
29
|
+
|
|
30
|
+
# Create a rate limiter that raises error when limit is exceeded
|
|
31
|
+
#
|
|
32
|
+
# @param limits [Array<Hash>] array of limit configurations
|
|
33
|
+
# @option limits [Integer] :window time window in seconds
|
|
34
|
+
# @option limits [Integer] :max_requests maximum requests allowed in the window
|
|
35
|
+
# @return [RateLimiter]
|
|
36
|
+
# @raise [ArgumentError] if limits is empty
|
|
37
|
+
def self.raise_on_limit(limits)
|
|
38
|
+
raise ArgumentError, "limits cannot be empty (use .no_limit for no rate limiting)" if limits.empty?
|
|
39
|
+
|
|
40
|
+
new(limits, Strategy::Raise.new)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Create a rate limiter that waits when limit is exceeded
|
|
44
|
+
#
|
|
45
|
+
# @param limits [Array<Hash>] array of limit configurations
|
|
46
|
+
# @option limits [Integer] :window time window in seconds
|
|
47
|
+
# @option limits [Integer] :max_requests maximum requests allowed in the window
|
|
48
|
+
# @return [RateLimiter]
|
|
49
|
+
# @raise [ArgumentError] if limits is empty
|
|
50
|
+
def self.wait_on_limit(limits)
|
|
51
|
+
raise ArgumentError, "limits cannot be empty (use .no_limit for no rate limiting)" if limits.empty?
|
|
52
|
+
|
|
53
|
+
new(limits, Strategy::Wait.new)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Create a null rate limiter that never limits
|
|
57
|
+
#
|
|
58
|
+
# @return [RateLimiter]
|
|
59
|
+
def self.no_limit = new([], Strategy::Null.new)
|
|
60
|
+
|
|
61
|
+
# Create a rate limiter with default limits (120 requests/min, 2000 requests/hour)
|
|
62
|
+
#
|
|
63
|
+
# These are the rate limits for the Wikiwiki REST API.
|
|
64
|
+
#
|
|
65
|
+
# @return [RateLimiter]
|
|
66
|
+
def self.default = wait_on_limit(WIKIWIKI_API_LIMITS)
|
|
67
|
+
|
|
68
|
+
private_class_method :new
|
|
69
|
+
|
|
70
|
+
# @param limits [Array<Hash>] array of limit configurations
|
|
71
|
+
# @param strategy [Strategy] rate limiting strategy
|
|
72
|
+
def initialize(limits, strategy)
|
|
73
|
+
@windows = limits.map {|limit| SlidingWindow.new(**limit) }
|
|
74
|
+
@mutex = Mutex.new
|
|
75
|
+
@strategy = strategy
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Acquire permission to make a request
|
|
79
|
+
#
|
|
80
|
+
# Behavior depends on the strategy:
|
|
81
|
+
# - raise_on_limit: raises RateLimitError if limit exceeded
|
|
82
|
+
# - wait_on_limit: waits until request can be made
|
|
83
|
+
# - no_limit: always succeeds immediately
|
|
84
|
+
#
|
|
85
|
+
# @return [void]
|
|
86
|
+
# @raise [RateLimitError] if rate limit is exceeded (with raise strategy)
|
|
87
|
+
def acquire!
|
|
88
|
+
@strategy.acquire!(self)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get time in seconds until next request can be made
|
|
92
|
+
#
|
|
93
|
+
# @return [Float] seconds to wait (0.0 if request can be made now)
|
|
94
|
+
def wait_time_until_available
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
@windows.map(&:wait_time).max || 0.0
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if a request can be made without exceeding any limits
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] true if request is allowed
|
|
103
|
+
def can_request?
|
|
104
|
+
@windows.all?(&:can_request?)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Record a new request across all window limiters
|
|
108
|
+
#
|
|
109
|
+
# @return [void]
|
|
110
|
+
def record!
|
|
111
|
+
@windows.each(&:record!)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get maximum wait time across all window limiters
|
|
115
|
+
#
|
|
116
|
+
# @return [Float, nil] seconds to wait
|
|
117
|
+
def wait_time
|
|
118
|
+
@windows.map(&:wait_time).max
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
attr_reader :mutex
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Rate limiting strategies
|
|
125
|
+
module Strategy
|
|
126
|
+
# Strategy that raises error when limit is exceeded
|
|
127
|
+
class Raise
|
|
128
|
+
# @param limiter [RateLimiter]
|
|
129
|
+
# @return [void]
|
|
130
|
+
# @raise [RateLimitError] if rate limit is exceeded
|
|
131
|
+
def acquire!(limiter)
|
|
132
|
+
limiter.mutex.synchronize do
|
|
133
|
+
raise RateLimitError, "Rate limit exceeded" unless limiter.can_request?
|
|
134
|
+
|
|
135
|
+
limiter.record!
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Strategy that waits when limit is exceeded
|
|
141
|
+
class Wait
|
|
142
|
+
# @param limiter [RateLimiter]
|
|
143
|
+
# @return [void]
|
|
144
|
+
def acquire!(limiter)
|
|
145
|
+
limiter.mutex.synchronize do
|
|
146
|
+
until limiter.can_request?
|
|
147
|
+
wait_time = limiter.wait_time
|
|
148
|
+
sleep wait_time if wait_time&.positive?
|
|
149
|
+
end
|
|
150
|
+
limiter.record!
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Strategy that never limits (null object pattern)
|
|
156
|
+
class Null
|
|
157
|
+
# @param limiter [RateLimiter]
|
|
158
|
+
# @return [void]
|
|
159
|
+
def acquire!(limiter)
|
|
160
|
+
# no-op
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wikiwiki
|
|
4
|
+
# Sliding window algorithm for rate limiting
|
|
5
|
+
class SlidingWindow
|
|
6
|
+
# Initialize a new window limiter
|
|
7
|
+
#
|
|
8
|
+
# @param window [Integer] time window in seconds
|
|
9
|
+
# @param max_requests [Integer] maximum requests allowed in the window
|
|
10
|
+
def initialize(window:, max_requests:)
|
|
11
|
+
@window = window
|
|
12
|
+
@max_requests = max_requests
|
|
13
|
+
@requests = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Check if a request can be made without exceeding the limit
|
|
17
|
+
#
|
|
18
|
+
# @return [Boolean] true if request is allowed
|
|
19
|
+
def can_request?
|
|
20
|
+
cleanup!
|
|
21
|
+
@requests.size < @max_requests
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Record a new request timestamp
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
27
|
+
def record!
|
|
28
|
+
@requests << Time.now
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get time in seconds until next request can be made
|
|
32
|
+
#
|
|
33
|
+
# @return [Float] seconds to wait (0.0 if request can be made now)
|
|
34
|
+
def wait_time
|
|
35
|
+
cleanup!
|
|
36
|
+
return 0.0 if @requests.size < @max_requests
|
|
37
|
+
|
|
38
|
+
# Time until oldest request expires
|
|
39
|
+
oldest = @requests.first
|
|
40
|
+
time_until_expiry = @window - (Time.now - oldest)
|
|
41
|
+
[time_until_expiry, 0.0].max
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private def cleanup!
|
|
45
|
+
cutoff = Time.now - @window
|
|
46
|
+
@requests.reject! {|timestamp| timestamp < cutoff }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "digest/md5"
|
|
5
|
+
require "erb"
|
|
6
|
+
require "logger"
|
|
7
|
+
require "time"
|
|
8
|
+
|
|
9
|
+
module Wikiwiki
|
|
10
|
+
# Represents a Wikiwiki wiki instance
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# auth = Wikiwiki::Auth.password(password: "admin_password")
|
|
14
|
+
# wiki = Wikiwiki::Wiki.new(wiki_id: "my-wiki", auth:)
|
|
15
|
+
# wiki.page_names # => ["FrontPage", "SideBar", ...]
|
|
16
|
+
# page = wiki.page(page_name: "FrontPage")
|
|
17
|
+
class Wiki
|
|
18
|
+
attr_reader :logger
|
|
19
|
+
|
|
20
|
+
private attr_reader :api, :wiki_id
|
|
21
|
+
|
|
22
|
+
# Initializes a new Wiki instance
|
|
23
|
+
#
|
|
24
|
+
# @param wiki_id [String] the wiki ID
|
|
25
|
+
# @param auth [Wikiwiki::Auth::Password, Wikiwiki::Auth::ApiKey] authentication credentials
|
|
26
|
+
# @param rate_limiter [RateLimiter] rate limiter instance (default: RateLimiter.default)
|
|
27
|
+
# @param logger [Logger] logger instance for API request/response logging (default: Logger.new($stdout))
|
|
28
|
+
# @raise [ArgumentError] if logger is explicitly set to nil
|
|
29
|
+
def initialize(wiki_id:, auth:, rate_limiter: RateLimiter.default, logger: Logger.new($stdout))
|
|
30
|
+
raise ArgumentError, "logger cannot be nil" if logger.nil?
|
|
31
|
+
|
|
32
|
+
@wiki_id = wiki_id
|
|
33
|
+
@logger = logger
|
|
34
|
+
@api = API.new(wiki_id:, auth:, logger:, rate_limiter:)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the wiki URL
|
|
38
|
+
#
|
|
39
|
+
# @return [URI::HTTPS] the frozen wiki URL
|
|
40
|
+
def url
|
|
41
|
+
@url ||= URI.parse("https://wikiwiki.jp/#{wiki_id}/").freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the list of all page names in the wiki
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<String>] array of page names
|
|
47
|
+
def page_names = api.get_pages["pages"].map {|p| p["name"] }
|
|
48
|
+
|
|
49
|
+
# Retrieves a page by name
|
|
50
|
+
#
|
|
51
|
+
# @param page_name [String] the name of the page
|
|
52
|
+
# @return [Wikiwiki::Page] the page object
|
|
53
|
+
# @raise [Wikiwiki::Error] if the page does not exist
|
|
54
|
+
def page(page_name:)
|
|
55
|
+
encoded_page_name = ERB::Util.url_encode(page_name)
|
|
56
|
+
page_data = api.get_page(encoded_page_name:)
|
|
57
|
+
Page.new(
|
|
58
|
+
name: page_name,
|
|
59
|
+
source: page_data["source"],
|
|
60
|
+
# NOTE: API returns timestamp as ISO8601 string for pages
|
|
61
|
+
timestamp: Time.iso8601(page_data["timestamp"])
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Updates a page with new content
|
|
66
|
+
#
|
|
67
|
+
# @param page_name [String] the name of the page to update
|
|
68
|
+
# @param source [String] the new page source content
|
|
69
|
+
# @return [void]
|
|
70
|
+
# @raise [Wikiwiki::Error] if the update fails
|
|
71
|
+
def update_page(page_name:, source:)
|
|
72
|
+
encoded_page_name = ERB::Util.url_encode(page_name)
|
|
73
|
+
api.put_page(encoded_page_name:, source:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Retrieves attachment file names on a page
|
|
77
|
+
#
|
|
78
|
+
# @param page_name [String] the name of the page
|
|
79
|
+
# @return [Array<String>] array of attachment file names
|
|
80
|
+
# @raise [Wikiwiki::Error] if the request fails
|
|
81
|
+
def attachment_names(page_name:)
|
|
82
|
+
encoded_page_name = ERB::Util.url_encode(page_name)
|
|
83
|
+
attachments_data = api.get_attachments(encoded_page_name:)
|
|
84
|
+
attachments = attachments_data["attachments"]
|
|
85
|
+
# API returns [] when no attachments, or Hash when attachments exist
|
|
86
|
+
attachments.is_a?(Hash) ? attachments.keys : []
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Retrieves a specific attachment on a page
|
|
90
|
+
#
|
|
91
|
+
# @param page_name [String] the name of the page
|
|
92
|
+
# @param attachment_name [String] the name of the attachment file
|
|
93
|
+
# @param rev [String, nil] optional MD5 hash to retrieve a specific revision
|
|
94
|
+
# @return [Wikiwiki::Attachment] the attachment object with decoded file data
|
|
95
|
+
# @raise [Wikiwiki::Error] if the attachment does not exist
|
|
96
|
+
# @raise [ArgumentError] if rev is not a valid MD5 hash format
|
|
97
|
+
def attachment(page_name:, attachment_name:, rev: nil)
|
|
98
|
+
if rev && !rev.match?(/\A[0-9a-f]{32}\z/i)
|
|
99
|
+
raise ArgumentError, "rev must be a valid MD5 hash (32 hexadecimal characters)"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
encoded_page_name = ERB::Util.url_encode(page_name)
|
|
103
|
+
encoded_attachment_name = ERB::Util.url_encode(attachment_name)
|
|
104
|
+
data = api.get_attachment(encoded_page_name:, encoded_attachment_name:, rev:)
|
|
105
|
+
|
|
106
|
+
content = Base64.strict_decode64(data["src"])
|
|
107
|
+
|
|
108
|
+
if content.bytesize != data["size"]
|
|
109
|
+
raise ContentIntegrityError, "Size mismatch: expected #{data["size"]}, got #{content.bytesize}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
calculated_md5 = Digest::MD5.hexdigest(content)
|
|
113
|
+
if calculated_md5 != data["md5hash"]
|
|
114
|
+
raise ContentIntegrityError, "MD5 hash mismatch: expected #{data["md5hash"]}, got #{calculated_md5}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Attachment.new(
|
|
118
|
+
page_name: data["page"],
|
|
119
|
+
name: data["file"],
|
|
120
|
+
size: data["size"],
|
|
121
|
+
# NOTE: API returns time as Unix timestamp (integer) for attachments, not ISO8601 string
|
|
122
|
+
time: Time.at(data["time"]),
|
|
123
|
+
type: data["type"],
|
|
124
|
+
content:
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Adds an attachment to a page
|
|
129
|
+
#
|
|
130
|
+
# @param page_name [String] the name of the page
|
|
131
|
+
# @param attachment_name [String] the name of the file
|
|
132
|
+
# @param content [String] the binary file content
|
|
133
|
+
# @return [void]
|
|
134
|
+
# @raise [Wikiwiki::Error] if the upload fails
|
|
135
|
+
def add_attachment(page_name:, attachment_name:, content:)
|
|
136
|
+
encoded_page_name = ERB::Util.url_encode(page_name)
|
|
137
|
+
encoded_content = Base64.strict_encode64(content)
|
|
138
|
+
api.put_attachment(encoded_page_name:, attachment_name:, encoded_content:)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Deletes an attachment from a page
|
|
142
|
+
#
|
|
143
|
+
# @param page_name [String] the name of the page
|
|
144
|
+
# @param attachment_name [String] the name of the attachment file
|
|
145
|
+
# @return [void]
|
|
146
|
+
# @raise [Wikiwiki::Error] if the deletion fails
|
|
147
|
+
def delete_attachment(page_name:, attachment_name:)
|
|
148
|
+
encoded_page_name = ERB::Util.url_encode(page_name)
|
|
149
|
+
encoded_attachment_name = ERB::Util.url_encode(attachment_name)
|
|
150
|
+
api.delete_attachment(encoded_page_name:, encoded_attachment_name:)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/wikiwiki.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require_relative "wikiwiki/version"
|
|
5
|
+
|
|
6
|
+
# Wikiwiki REST API client library
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# auth = Wikiwiki::Auth.password(password: "admin_password")
|
|
10
|
+
# wiki = Wikiwiki::Wiki.new(wiki_id: "my-wiki", auth:)
|
|
11
|
+
# page = wiki.page(page_name: "FrontPage")
|
|
12
|
+
# page.name # => "FrontPage"
|
|
13
|
+
module Wikiwiki
|
|
14
|
+
# Base error class for Wikiwiki gem
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Rate limit exceeded error
|
|
18
|
+
class RateLimitError < Error; end
|
|
19
|
+
|
|
20
|
+
# Content integrity error
|
|
21
|
+
# Raised when downloaded file content doesn't match expected size or MD5 hash
|
|
22
|
+
class ContentIntegrityError < Error; end
|
|
23
|
+
|
|
24
|
+
# API request error
|
|
25
|
+
class APIError < Error; end
|
|
26
|
+
|
|
27
|
+
# Authentication error
|
|
28
|
+
# Raised when HTTP status is 401
|
|
29
|
+
class AuthenticationError < APIError; end
|
|
30
|
+
|
|
31
|
+
# Resource not found error
|
|
32
|
+
# Raised when HTTP status is 404
|
|
33
|
+
class ResourceNotFoundError < APIError; end
|
|
34
|
+
|
|
35
|
+
# Server error
|
|
36
|
+
# Raised when HTTP status is 5xx
|
|
37
|
+
class ServerError < APIError; end
|
|
38
|
+
|
|
39
|
+
loader = Zeitwerk::Loader.for_gem
|
|
40
|
+
loader.inflector.inflect("api" => "API")
|
|
41
|
+
loader.setup
|
|
42
|
+
loader.eager_load
|
|
43
|
+
end
|
data/mise.toml
ADDED
data/rbs_collection.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Download sources
|
|
2
|
+
sources:
|
|
3
|
+
- type: stdlib
|
|
4
|
+
|
|
5
|
+
# A directory to install the downloaded RBSs
|
|
6
|
+
path: .gem_rbs_collection
|
|
7
|
+
|
|
8
|
+
# A directory to install the downloaded RBSs
|
|
9
|
+
gems:
|
|
10
|
+
# Use standard library type definitions
|
|
11
|
+
- name: net-http
|
|
12
|
+
- name: uri
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Wikiwiki
|
|
2
|
+
class API
|
|
3
|
+
attr_reader logger: Logger
|
|
4
|
+
|
|
5
|
+
def initialize: (wiki_id: String, auth: Auth::Password | Auth::ApiKey, logger: Logger, ?rate_limiter: RateLimiter) -> void
|
|
6
|
+
|
|
7
|
+
def get_pages: () -> Hash[String, Array[Hash[String, String]]]
|
|
8
|
+
|
|
9
|
+
def get_page: (encoded_page_name: String) -> Hash[String, String]
|
|
10
|
+
|
|
11
|
+
def put_page: (encoded_page_name: String, source: String) -> void
|
|
12
|
+
|
|
13
|
+
def get_attachments: (encoded_page_name: String) -> Hash[String, Hash[String, Hash[String, untyped]]]
|
|
14
|
+
|
|
15
|
+
def get_attachment: (encoded_page_name: String, encoded_attachment_name: String, ?rev: String?) -> Hash[String, untyped]
|
|
16
|
+
|
|
17
|
+
def put_attachment: (encoded_page_name: String, attachment_name: String, encoded_content: String) -> void
|
|
18
|
+
|
|
19
|
+
def delete_attachment: (encoded_page_name: String, encoded_attachment_name: String) -> void
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def authenticate: (Auth::Password | Auth::ApiKey auth) -> String
|
|
24
|
+
|
|
25
|
+
def parse_json_response: (Net::HTTPResponse response) -> Hash[String, untyped]
|
|
26
|
+
|
|
27
|
+
def request: (Symbol method, URI::HTTPS uri, ?body: Hash[String, String]?, ?authenticate: bool) -> Net::HTTPResponse
|
|
28
|
+
|
|
29
|
+
def log_request: (Symbol method, URI::HTTPS uri, Net::HTTPRequest request) -> void
|
|
30
|
+
|
|
31
|
+
def log_response: (Net::HTTPResponse response) -> void
|
|
32
|
+
|
|
33
|
+
attr_reader wiki_id: String
|
|
34
|
+
attr_reader token: String
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Wikiwiki
|
|
2
|
+
class Attachment
|
|
3
|
+
attr_reader page_name: String
|
|
4
|
+
attr_reader name: String
|
|
5
|
+
attr_reader size: Integer
|
|
6
|
+
attr_reader time: Time
|
|
7
|
+
attr_reader type: String
|
|
8
|
+
attr_reader content: String
|
|
9
|
+
|
|
10
|
+
def initialize: (page_name: String, name: String, size: Integer, time: Time, type: String, content: String) -> void
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Wikiwiki
|
|
2
|
+
class RateLimiter
|
|
3
|
+
def self.raise_on_limit: (Array[{ window: Integer, max_requests: Integer }] limits) -> RateLimiter
|
|
4
|
+
|
|
5
|
+
def self.wait_on_limit: (Array[{ window: Integer, max_requests: Integer }] limits) -> RateLimiter
|
|
6
|
+
|
|
7
|
+
def self.no_limit: () -> RateLimiter
|
|
8
|
+
|
|
9
|
+
def self.default: () -> RateLimiter
|
|
10
|
+
|
|
11
|
+
def acquire!: () -> void
|
|
12
|
+
|
|
13
|
+
def wait_time_until_available: () -> Float
|
|
14
|
+
|
|
15
|
+
def can_request?: () -> bool
|
|
16
|
+
|
|
17
|
+
def record!: () -> void
|
|
18
|
+
|
|
19
|
+
def wait_time: () -> Float?
|
|
20
|
+
|
|
21
|
+
attr_reader mutex: Thread::Mutex
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module Strategy
|
|
25
|
+
interface _Strategy
|
|
26
|
+
def acquire!: (RateLimiter limiter) -> void
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Raise
|
|
30
|
+
include _Strategy
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Wait
|
|
34
|
+
include _Strategy
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Null
|
|
38
|
+
include _Strategy
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Wikiwiki
|
|
2
|
+
class SlidingWindow
|
|
3
|
+
def initialize: (window: Integer, max_requests: Integer) -> void
|
|
4
|
+
|
|
5
|
+
def can_request?: () -> bool
|
|
6
|
+
|
|
7
|
+
def record!: () -> void
|
|
8
|
+
|
|
9
|
+
def wait_time: () -> Float
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def cleanup!: () -> void
|
|
14
|
+
|
|
15
|
+
attr_reader window: Integer
|
|
16
|
+
attr_reader max_requests: Integer
|
|
17
|
+
attr_reader requests: Array[Time]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Wikiwiki
|
|
2
|
+
class Wiki
|
|
3
|
+
attr_reader logger: Logger
|
|
4
|
+
|
|
5
|
+
def initialize: (wiki_id: String, auth: Auth::Password | Auth::ApiKey, ?rate_limiter: RateLimiter, ?logger: Logger) -> void
|
|
6
|
+
|
|
7
|
+
def url: () -> URI::HTTPS
|
|
8
|
+
|
|
9
|
+
def page_names: () -> Array[String]
|
|
10
|
+
|
|
11
|
+
def page: (page_name: String) -> Page
|
|
12
|
+
|
|
13
|
+
def update_page: (page_name: String, source: String) -> void
|
|
14
|
+
|
|
15
|
+
def attachment_names: (page_name: String) -> Array[String]
|
|
16
|
+
|
|
17
|
+
def attachment: (page_name: String, attachment_name: String, ?rev: String?) -> Attachment
|
|
18
|
+
|
|
19
|
+
def add_attachment: (page_name: String, attachment_name: String, content: String) -> void
|
|
20
|
+
|
|
21
|
+
def delete_attachment: (page_name: String, attachment_name: String) -> void
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader api: API
|
|
26
|
+
attr_reader wiki_id: String
|
|
27
|
+
end
|
|
28
|
+
end
|
data/sig/wikiwiki.rbs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Wikiwiki
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class RateLimitError < Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class ContentIntegrityError < Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class APIError < Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class AuthenticationError < APIError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class ResourceNotFoundError < APIError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class ServerError < APIError
|
|
23
|
+
end
|
|
24
|
+
end
|