git_hub_bub 1.0.1 → 2.0.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 +4 -4
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ci.yml +43 -0
- data/.standard.yml +1 -0
- data/Gemfile +2 -2
- data/README.md +1 -1
- data/Rakefile +8 -9
- data/changelog.md +6 -1
- data/git_hub_bub.gemspec +17 -17
- data/lib/git_hub_bub/request.rb +31 -28
- data/lib/git_hub_bub/response.rb +85 -83
- data/lib/git_hub_bub/version.rb +1 -1
- data/lib/git_hub_bub.rb +11 -13
- data/test/git_hub_bub/request_test.rb +42 -43
- data/test/git_hub_bub/response_test.rb +34 -18
- data/test/git_hub_bub/valid_token_test.rb +16 -14
- data/test/test_helper.rb +28 -31
- metadata +44 -28
- data/.travis.yml +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a1fb065fefd4671e1af0e588b6cfb6ea5d19261d61af52196c3a6e78eb2640a
|
|
4
|
+
data.tar.gz: 6904fbfcd325633d8de9d6a332d8fe0700e58685b66d612598a007879526affc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8a89709d49289a988464527e23b88e8eaad3115d26714d50df2679078e06f0d735c08a5b06487f56b6619b15d90022c37031b345d91f83f8c69efe7d58d7ec8
|
|
7
|
+
data.tar.gz: 53fd2d767153a4ba3e38d7e8e8e5cbc747ddee4797247e3584771f5605c036a24a73e2e5c8721b88b548502729cf2aec9248b5d7ea2c2d81ee366e3acde2f9d3
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
# Avoid duplicate builds on PRs.
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
pull_request:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: '3.2'
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Run StandardRB
|
|
23
|
+
run: bundle exec standardrb
|
|
24
|
+
|
|
25
|
+
test:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
|
|
28
|
+
strategy:
|
|
29
|
+
fail-fast: false
|
|
30
|
+
matrix:
|
|
31
|
+
ruby-version: ['3.2', '3.3', '3.4', '4.0']
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v6
|
|
35
|
+
|
|
36
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
37
|
+
uses: ruby/setup-ruby@v1
|
|
38
|
+
with:
|
|
39
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
40
|
+
bundler-cache: true
|
|
41
|
+
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: bundle exec rake test
|
data/.standard.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby_version: 3.2
|
data/Gemfile
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
source
|
|
1
|
+
source "https://rubygems.org"
|
|
2
2
|
|
|
3
|
-
gemspec
|
|
3
|
+
gemspec
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GitHubBub
|
|
2
2
|
|
|
3
|
-
[](https://github.com/schneems/git_hub_bub/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.codetriage.com/schneems/git_hub_bub)
|
|
5
5
|
|
|
6
6
|
A low level GitHub client that makes the disgusting issue of header based url pagination simple.
|
data/Rakefile
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "git_hub_bub"
|
|
3
3
|
|
|
4
|
-
task :
|
|
4
|
+
task default: [:test]
|
|
5
5
|
|
|
6
|
-
require
|
|
7
|
-
require
|
|
6
|
+
require "rake"
|
|
7
|
+
require "rake/testtask"
|
|
8
8
|
|
|
9
9
|
Rake::TestTask.new(:test) do |t|
|
|
10
|
-
t.libs <<
|
|
11
|
-
t.libs <<
|
|
12
|
-
t.pattern =
|
|
10
|
+
t.libs << "lib"
|
|
11
|
+
t.libs << "test"
|
|
12
|
+
t.pattern = "test/**/*_test.rb"
|
|
13
13
|
t.verbose = true
|
|
14
14
|
end
|
|
15
|
-
|
data/changelog.md
CHANGED
data/git_hub_bub.gemspec
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
require
|
|
3
|
+
require "git_hub_bub/version"
|
|
5
4
|
|
|
6
5
|
Gem::Specification.new do |gem|
|
|
7
|
-
gem.name
|
|
8
|
-
gem.version
|
|
9
|
-
gem.authors
|
|
10
|
-
gem.email
|
|
11
|
-
gem.description
|
|
12
|
-
gem.summary
|
|
13
|
-
gem.homepage
|
|
14
|
-
gem.license
|
|
6
|
+
gem.name = "git_hub_bub"
|
|
7
|
+
gem.version = GitHubBub::VERSION
|
|
8
|
+
gem.authors = ["Richard Schneeman"]
|
|
9
|
+
gem.email = ["richard.schneeman+rubygems@gmail.com"]
|
|
10
|
+
gem.description = "git_hub_bub makes github requests"
|
|
11
|
+
gem.summary = "git_hub_bub makes github requests"
|
|
12
|
+
gem.homepage = "https://github.com/schneems/git_hub_bub"
|
|
13
|
+
gem.license = "MIT"
|
|
15
14
|
|
|
16
|
-
gem.files
|
|
17
|
-
gem.executables
|
|
18
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
15
|
+
gem.files = `git ls-files`.split($/)
|
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
19
17
|
gem.require_paths = ["lib"]
|
|
20
18
|
|
|
21
19
|
gem.add_dependency "rrrretry"
|
|
22
20
|
gem.add_dependency "excon"
|
|
21
|
+
gem.add_dependency "base64"
|
|
23
22
|
gem.add_development_dependency "timecop"
|
|
24
23
|
gem.add_development_dependency "test-unit"
|
|
25
24
|
gem.add_development_dependency "mocha"
|
|
26
25
|
gem.add_development_dependency "rake"
|
|
27
|
-
gem.add_development_dependency "vcr"
|
|
28
|
-
gem.add_development_dependency "webmock"
|
|
26
|
+
gem.add_development_dependency "vcr"
|
|
27
|
+
gem.add_development_dependency "webmock"
|
|
29
28
|
gem.add_development_dependency "dotenv"
|
|
29
|
+
gem.add_development_dependency "standard"
|
|
30
30
|
|
|
31
|
-
gem.required_ruby_version =
|
|
31
|
+
gem.required_ruby_version = ">= 3.2"
|
|
32
32
|
end
|
data/lib/git_hub_bub/request.rb
CHANGED
|
@@ -4,22 +4,23 @@ module GitHubBub
|
|
|
4
4
|
class RequestError < StandardError; end
|
|
5
5
|
|
|
6
6
|
class Request
|
|
7
|
-
attr_accessor :url, :options
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
attr_accessor :url, :options
|
|
8
|
+
attr_writer :token
|
|
9
|
+
BASE_URI = "https://api.github.com"
|
|
10
|
+
USER_AGENT = defined?(USER_AGENT) ? USER_AGENT : SecureRandom.hex(16)
|
|
10
11
|
GITHUB_VERSION = "vnd.github.3.raw+json"
|
|
11
|
-
EXTRA_HEADERS
|
|
12
|
-
BASE_HEADERS
|
|
13
|
-
BASE_OPTIONS
|
|
14
|
-
RETRIES
|
|
15
|
-
RAISE_ON_FAIL
|
|
12
|
+
EXTRA_HEADERS = defined?(EXTRA_HEADERS) ? EXTRA_HEADERS : {}
|
|
13
|
+
BASE_HEADERS = EXTRA_HEADERS.merge({"Accept" => "application/#{GITHUB_VERSION}", "User-Agent" => USER_AGENT})
|
|
14
|
+
BASE_OPTIONS = {omit_default_port: true}
|
|
15
|
+
RETRIES = 1
|
|
16
|
+
RAISE_ON_FAIL = ENV["GIT_HUB_BUB_RAISE_ON_FAIL"]
|
|
16
17
|
|
|
17
18
|
def initialize(url, query = {}, options = {})
|
|
18
|
-
self.url
|
|
19
|
-
@skip_token
|
|
20
|
-
self.options
|
|
21
|
-
self.options[:query]
|
|
22
|
-
self.options[:headers] = BASE_HEADERS.merge(options[:headers]|| {})
|
|
19
|
+
self.url = /^http(\w?):\/\//.match?(url) ? url : File.join(BASE_URI, url)
|
|
20
|
+
@skip_token = options.delete(:skip_token)
|
|
21
|
+
self.options = BASE_OPTIONS.merge(options || {})
|
|
22
|
+
self.options[:query] = query if query && !query.empty?
|
|
23
|
+
self.options[:headers] = BASE_HEADERS.merge(options[:headers] || {})
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def skip_token?
|
|
@@ -27,7 +28,7 @@ module GitHubBub
|
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def self.head(url, query = {}, options = {})
|
|
30
|
-
|
|
31
|
+
new(url, query, options).head
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def head
|
|
@@ -37,19 +38,21 @@ module GitHubBub
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def self.get(url, query = {}, options = {})
|
|
40
|
-
|
|
41
|
+
new(url, query, options).get
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def get
|
|
44
45
|
wrap_request do
|
|
45
46
|
ex = Excon.get(url, options)
|
|
46
|
-
|
|
47
|
+
if (@location = ex.headers["Location"])
|
|
48
|
+
ex = Excon.get(@location, options)
|
|
49
|
+
end
|
|
47
50
|
ex
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def self.post(url, query = {}, options = {})
|
|
52
|
-
|
|
55
|
+
new(url, query, options).post
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def post
|
|
@@ -59,7 +62,7 @@ module GitHubBub
|
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
def self.patch(url, query = {}, options = {})
|
|
62
|
-
|
|
65
|
+
new(url, query, options).patch
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def patch
|
|
@@ -69,7 +72,7 @@ module GitHubBub
|
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
def self.put(url, query = {}, options = {})
|
|
72
|
-
|
|
75
|
+
new(url, query, options).put
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def put
|
|
@@ -79,7 +82,7 @@ module GitHubBub
|
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
def self.delete(url, query = {}, options = {})
|
|
82
|
-
|
|
85
|
+
new(url, query, options).delete
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
def delete
|
|
@@ -97,9 +100,9 @@ module GitHubBub
|
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
if RAISE_ON_FAIL
|
|
100
|
-
raise RequestError, "message: '#{response.json_body[
|
|
103
|
+
raise RequestError, "message: '#{response.json_body["message"]}', url: '#{url}', response: '#{response.inspect}'" unless response.success?
|
|
101
104
|
end
|
|
102
|
-
|
|
105
|
+
response
|
|
103
106
|
end
|
|
104
107
|
|
|
105
108
|
# do they take query params? do they take :body?
|
|
@@ -114,22 +117,22 @@ module GitHubBub
|
|
|
114
117
|
end
|
|
115
118
|
|
|
116
119
|
def token
|
|
117
|
-
@token ||= if options[:headers] && token_string = options[:headers]["Authorization"]
|
|
120
|
+
@token ||= if options[:headers] && (token_string = options[:headers]["Authorization"])
|
|
118
121
|
token_string.split(/\s/).last
|
|
119
|
-
elsif options[:query] &&
|
|
120
|
-
|
|
122
|
+
elsif options[:query] && (query_token = options[:query].delete(:token))
|
|
123
|
+
query_token
|
|
121
124
|
else
|
|
122
125
|
skip_token?
|
|
123
126
|
end
|
|
124
127
|
end
|
|
125
|
-
|
|
128
|
+
alias_method :token?, :token
|
|
126
129
|
|
|
127
130
|
def self.set_before_callback(&block)
|
|
128
131
|
before_callbacks << block
|
|
129
132
|
end
|
|
130
133
|
|
|
131
134
|
def self.before_callbacks
|
|
132
|
-
@before_callbacks ||=[]
|
|
135
|
+
@before_callbacks ||= []
|
|
133
136
|
end
|
|
134
137
|
|
|
135
138
|
def self.clear_callbacks
|
|
@@ -138,7 +141,7 @@ module GitHubBub
|
|
|
138
141
|
|
|
139
142
|
def before_callbacks!
|
|
140
143
|
self.class.before_callbacks.each do |callback|
|
|
141
|
-
run_callback
|
|
144
|
+
run_callback(&callback)
|
|
142
145
|
end
|
|
143
146
|
end
|
|
144
147
|
|
data/lib/git_hub_bub/response.rb
CHANGED
|
@@ -1,104 +1,106 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "date"
|
|
4
4
|
|
|
5
5
|
module GitHubBub
|
|
6
6
|
class Response < Excon::Response
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
def self.create(response)
|
|
8
|
+
new(response.data)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def rate_limit_remaining
|
|
12
|
+
limit_remaining = headers["X-RateLimit-Limit"]
|
|
13
|
+
Integer(limit_remaining)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rate_limit_reset_time_left # in seconds
|
|
17
|
+
utc_epoch_seconds = headers["X-RateLimit-Reset"]
|
|
18
|
+
utc_epoch_seconds = Integer(utc_epoch_seconds)
|
|
19
|
+
utc_epoch_seconds - Time.now.utc.to_i
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# When no time is left we want to sleep until our limit is reset
|
|
23
|
+
# i.e. remaining is 1 so time/1 => time
|
|
24
|
+
#
|
|
25
|
+
# When we have plenty of requests left then we want to sleep for too long
|
|
26
|
+
# i.e. time / 1000 => smaller amount of time
|
|
27
|
+
def rate_limit_sleep!(bypass_sleep: false)
|
|
28
28
|
remaining = rate_limit_remaining
|
|
29
29
|
time_left = rate_limit_reset_time_left
|
|
30
30
|
return 0 if time_left <= 0
|
|
31
31
|
return 0 if remaining > 1000
|
|
32
32
|
|
|
33
|
-
if remaining > 0
|
|
34
|
-
|
|
33
|
+
val = if remaining > 0
|
|
34
|
+
time_left / remaining.to_f
|
|
35
35
|
else
|
|
36
|
-
|
|
36
|
+
time_left
|
|
37
37
|
end
|
|
38
38
|
sleep(val) unless bypass_sleep
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
val
|
|
40
|
+
end
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
::JSON.parse(
|
|
44
|
-
|
|
42
|
+
def json_body
|
|
43
|
+
::JSON.parse(body)
|
|
44
|
+
end
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
def success?
|
|
47
47
|
status.to_s =~ /^2.*/
|
|
48
|
-
|
|
48
|
+
end
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
def pagination
|
|
51
|
+
@pagination ||= parse_pagination
|
|
52
|
+
end
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
def parsed_response
|
|
55
55
|
response.body.inspect
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def next_url
|
|
59
|
+
pagination["next_url"]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def prev_url
|
|
63
|
+
pagination["prev_url"]
|
|
64
|
+
end
|
|
65
|
+
alias_method :previous_url, :prev_url
|
|
66
|
+
|
|
67
|
+
def last_url
|
|
68
|
+
pagination["last_url"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def first_url
|
|
72
|
+
pagination["first_url"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def last_page?
|
|
76
|
+
return true if next_url.nil?
|
|
77
|
+
return false if last_url.nil? # A next page exists, but no "last" page, keep going.
|
|
78
|
+
last_page_number = page_number_from_url(last_url)
|
|
79
|
+
next_page_number = page_number_from_url(next_url)
|
|
80
|
+
next_page_number > last_page_number
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def first_page?
|
|
84
|
+
return true if first_url.nil?
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def page_number_from_url(url)
|
|
89
|
+
query = ::URI.parse(url).query
|
|
90
|
+
params = ::URI.decode_www_form(query).to_h
|
|
91
|
+
params["page"].to_i
|
|
92
|
+
end
|
|
91
93
|
|
|
92
94
|
def header_links
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
95
|
+
(headers["link"] || headers["Link"] || "").split(",")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_pagination
|
|
99
|
+
header_links.each_with_object({}) do |element, hash|
|
|
100
|
+
key = element[/rel=["'](.*)['"]/, 1]
|
|
101
|
+
value = element[/<(.*)>/, 1]
|
|
102
|
+
hash["#{key}_url"] = value
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/git_hub_bub/version.rb
CHANGED
data/lib/git_hub_bub.rb
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require 'cgi'
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
7
6
|
|
|
8
|
-
require
|
|
9
|
-
require
|
|
7
|
+
require "excon"
|
|
8
|
+
require "rrrretry"
|
|
10
9
|
|
|
11
|
-
require
|
|
12
|
-
require
|
|
10
|
+
require "git_hub_bub/request"
|
|
11
|
+
require "git_hub_bub/response"
|
|
13
12
|
|
|
14
13
|
module GitHubBub
|
|
15
14
|
class << self
|
|
16
|
-
|
|
17
15
|
def valid_token?(token)
|
|
18
|
-
response = Request.get("https://#{ENV[
|
|
16
|
+
response = Request.get("https://#{ENV["GITHUB_APP_ID"]}:#{ENV["GITHUB_APP_SECRET"]}@api.github.com/applications/#{ENV["GITHUB_APP_ID"]}/tokens/#{token}", {}, {skip_token: true})
|
|
19
17
|
return response if response.success?
|
|
20
|
-
|
|
18
|
+
false if response.status == 404
|
|
21
19
|
rescue GitHubBub::RequestError => e
|
|
22
20
|
if Request::RAISE_ON_FAIL
|
|
23
|
-
|
|
21
|
+
false
|
|
24
22
|
else
|
|
25
23
|
raise e
|
|
26
24
|
end
|
|
@@ -50,4 +48,4 @@ module GitHubBub
|
|
|
50
48
|
Request.delete(*args)
|
|
51
49
|
end
|
|
52
50
|
end
|
|
53
|
-
end
|
|
51
|
+
end
|