nonacat 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 64443b38f33c777d82e6348617f50ec2df383c2132f56fdc5ace21f913d8f0b4
4
+ data.tar.gz: 28918752d3adac56bf5858d16be3b0e8723d440aa6c218ccf7136abdc5c52e65
5
+ SHA512:
6
+ metadata.gz: afd3f0c68d1a65d9a5aeb80d9d01fc46ab18034e57df08ed2c8325e5030afc2a1c18ef88741ee45085c6fb8f378e344015d234240bbb89545d5e865dddf89afb
7
+ data.tar.gz: 1af24f360628d1e8512e771c41b2263126a69be7ba8de658ecf6b8df1b97031d99c583a33d5efac9048086b81557fafce10d470338896d54142dfd8837943af2
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --main README.md
2
+ --markup=markdown
3
+ --markup-provider=redcarpet
4
+ --no-private
5
+ lib/**/*.rb
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Ethan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # Nonacat
2
+
3
+ A Github API client - unofficial, unaffiliated
4
+
5
+ Nonacat uses [Scorpio](https://github.com/notEthan/scorpio) with [Github's OpenAPI description](https://github.com/github/rest-api-description) to be a client to the service. Nonacat builds a small amount of infrastructure to simplify things like authentication and pagination, but otherwise relies wholly on the OpenAPI document for implementation of the client.
6
+
7
+ ## Usage
8
+
9
+ Nonacat is built on [Scorpio](https://github.com/notEthan/scorpio), which adds functionality to an OpenAPI document, letting the document be used as a client to the service it describes. Scorpio is in turn built on [JSI](https://github.com/notEthan/jsi). Some familiarity with both is useful in using Nonacat. Nonacat's own codebase is very small - Github's [octokit.rb](https://github.com/octokit/octokit.rb) is currently about 22,000 lines of code; Nonacat is about 100.
10
+
11
+ ### Caveats
12
+
13
+ Github's OpenAPI description is quite large - 11 MB as of this writing, and the whole thing is loaded and instantiated as the client. This can be unwieldy if you do things that iterate the whole document. For example, on my machine inspecting the document (`Nonacat::GITHUB_API.inspect`) takes a full 3 minutes (though only the first time; subsequent calls are much faster as computations are cached). This is a problem if, for example, you call a method that does not exist on a node in the document; when a `NoMethodError` is raised, the receiver is inspected, resulting in an error message that is very large and slow to generate.
14
+
15
+ ### Authentication
16
+
17
+ Github authentication credentials, which are documented at <https://docs.github.com/en/rest/authentication>, are passed to [Faraday::Request::Authorization](https://rubydoc.info/gems/faraday/Faraday/Request/Authorization) from {Nonacat.authorization}.
18
+
19
+ Authentication typically looks like:
20
+
21
+ ```ruby
22
+ Nonacat.authorization = ['Bearer', 'github_pat_2kxqIkfByCRkCGT2...']
23
+ # or
24
+ Nonacat.authorization = [:basic, 'notEthan', 'p4$$w0rd']
25
+ ```
26
+
27
+ ### Operations
28
+
29
+ Requests to the API are made using an OpenAPI Operation (a [`Scorpio::OpenAPI::Operation`](https://rubydoc.info/gems/scorpio/Scorpio/OpenAPI/Operation)), a part of the OpenAPI description that describes the form of the request and response. An operation can be identified by a templated path and HTTP method, or by id (the `operationId` property of the operation).
30
+
31
+ For example, the operation to [get a repository](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#get-a-repository) is a HTTP get request to `/repos/{owner}/{repo}`, accessed in the OpenAPI document like so:
32
+
33
+ ```ruby
34
+ get_repo_operation = Nonacat::GITHUB_API.paths['/repos/{owner}/{repo}'].get
35
+ ```
36
+
37
+ Its id is `repos/get` (from `get_repo_operation.operationId`). You can use such an id to retrieve an operation, e.g. `Nonacat::GITHUB_API.operations['repos/get']`. Finding the id of an operation can be slightly inconvenient as it is not included on Github's HTML pages of API documentation. Available `operationId`s can be iterated with e.g. `Nonacat::GITHUB_API.operations.map(&:operationId)` or `Nonacat::GITHUB_API.operations.tagged("gists").map(&:operationId)`.
38
+
39
+ ### `nonacat` executable
40
+
41
+ Nonacat includes an executable `nonacat`, which is just IRB with nonacat loaded and some additions for convenience:
42
+
43
+ - Authentication is loaded from the same source as the github [`gh` CLI](https://cli.github.com/), if available.
44
+ - Tab-completable references to operations are defined. Github's operations are categorized, e.g. the `repos/get` operation with category `repos`. The `nonacat` executable defines constants like `Nonacat::REPOS` for each category, which in turn contain constants for each operation. With these, `Nonacat::REPOS::GET` refers to the same operation as `Nonacat::GITHUB_API.operations['repos/get']`.
45
+
46
+ ### Links
47
+
48
+ Many Github resources link to other resources with inline URLs, e.g. a repo resource has a `forks_url` property linking to the `repos/list-forks` operation's path. Nonacat extends these URLs with {Nonacat::Link} and the linked resource can be retrieved with `#get`, e.g. `forks = my_repo.forks_url.get`. (See the example "Get linked repository forks" below.)
49
+
50
+ ### Pagination
51
+
52
+ Many Github API operations paginate results. {Nonacat.paginate_items} abstracts pagination - see its method doc, and examples below.
53
+
54
+ ### Examples
55
+
56
+ - Get Zen (no auth required)
57
+
58
+ ```ruby
59
+ Nonacat::GITHUB_API.operations["meta/get-zen"].run
60
+ # => "Non-blocking is better than blocking."
61
+ ```
62
+
63
+ - Get repository
64
+
65
+ ```
66
+ repo = Nonacat::GITHUB_API.operations["repos/get"].run(owner: 'notEthan', repo: 'scorpio')
67
+ ```
68
+
69
+ Returns (trimmed)
70
+
71
+ ```
72
+ #{<JSI (Nonacat::Github::FullRepository)>
73
+ "id" => 69611598,
74
+ "name" => "scorpio",
75
+ "full_name" => "notEthan/scorpio",
76
+ "owner" => #{<JSI (Nonacat::Github::SimpleUser)>
77
+ "login" => "notEthan",
78
+ },
79
+ "url" => #<JSI (Nonacat::Github::FullRepository.properties["url"]) "https://api.github.com/repos/notEthan/scorpio">,
80
+ "forks_url" => #<JSI (Nonacat::Github::FullRepository.properties["forks_url"]) "https://api.github.com/repos/notEthan/scorpio/forks">,
81
+ "language" => "Ruby",
82
+ }
83
+ ```
84
+
85
+ - Get linked repository forks (using `repo` from previous example)
86
+
87
+ ```
88
+ forks = repo.forks_url.get
89
+ ```
90
+
91
+ That connects the `forks_url` to the `repos/list-forks` operation, essentially running `forks = Nonacat.operations["repos/list-forks"].run(owner: 'notEthan', repo: 'scorpio')`
92
+
93
+ Returns (trimmed)
94
+
95
+ ```
96
+ #[<JSI (Nonacat::Github.paths["/repos/{owner}/{repo}/forks"].get.responses["200"].content["application/json"].schema)>
97
+ #{<JSI (Nonacat::Github::MinimalRepository)>
98
+ "id" => 86715358,
99
+ "name" => "scorpio",
100
+ "full_name" => "mathieujobin/scorpio",
101
+ "owner" => #{<JSI (Nonacat::Github::SimpleUser)>
102
+ "login" => "mathieujobin",
103
+ },
104
+ "fork" => true,
105
+ "url" => #<JSI (Nonacat::Github::MinimalRepository.properties["url"]) "https://api.github.com/repos/mathieujobin/scorpio">,
106
+ "forks_url" => #<JSI (Nonacat::Github::MinimalRepository.properties["forks_url"]) "https://api.github.com/repos/mathieujobin/scorpio/forks">,
107
+ "language" => "Ruby",
108
+ }
109
+ ]
110
+ ```
111
+
112
+ - Search code, paginated (requires auth) - this pauses between each item; press enter to continue or `q` + enter to quit.
113
+
114
+ ```ruby
115
+ Nonacat.paginate_items('search/code', q: 'nonacat', per_page: 4) do |item|
116
+ pp(item)
117
+ break if gets.chomp == 'q'
118
+ end
119
+ ```
120
+
121
+ Output (trimmed):
122
+
123
+ ```
124
+ #{<JSI (Nonacat::Github::CodeSearchResultItem)>
125
+ "name" => "nonacat.rb",
126
+ "path" => "lib/nonacat.rb",
127
+ "url" => #<JSI (Nonacat::Github::CodeSearchResultItem.properties["url"])
128
+ "https://api.github.com/repositories/898892904/contents/lib/nonacat.rb?ref=a253ff2a2c9b1229f2feea63f22a6ba7b21d1dd3"
129
+ >,
130
+ "repository" => #{<JSI (Nonacat::Github::MinimalRepository)>
131
+ "name" => "nonacat",
132
+ "full_name" => "notEthan/nonacat",
133
+ "owner" => #{<JSI (Nonacat::Github::SimpleUser)>
134
+ "login" => "notEthan",
135
+ },
136
+ "html_url" => #<JSI (Nonacat::Github::MinimalRepository.properties["html_url"])
137
+ "https://github.com/notEthan/nonacat"
138
+ >,
139
+ },
140
+ "score" => 1.0
141
+ }
142
+ ```
143
+
144
+ - Create a gist
145
+
146
+ ```ruby
147
+ gist = Nonacat::GITHUB_API.operations['gists/create'].run(
148
+ body_object: {
149
+ description: "test #{rand(1000)}",
150
+ files: {
151
+ 'foo.rb' => {content: 'require "nonacat"'}
152
+ },
153
+ public: true,
154
+ }
155
+ )
156
+ ```
157
+
158
+ Returns (trimmed)
159
+
160
+ ```
161
+ #{<JSI (Nonacat::Github::GistSimple)>
162
+ "url" => "https://api.github.com/gists/729cbe8c58e7698702af6a5c51d45725",
163
+ "html_url" => "https://gist.github.com/notEthan/729cbe8c58e7698702af6a5c51d45725",
164
+ "files" => #{<JSI (Nonacat::Github::GistSimple.properties["files"])>
165
+ "foo.rb" => #{<JSI (Nonacat::Github::GistSimple.properties["files"].additionalProperties)>
166
+ "filename" => "foo.rb",
167
+ "language" => "Ruby",
168
+ "content" => "require \"nonacat\"",
169
+ }
170
+ },
171
+ "description" => "test 3",
172
+ "owner" => #{<JSI (Nonacat::Github::SimpleUser)>
173
+ "login" => "notEthan",
174
+ },
175
+ }
176
+ ```
177
+
178
+ - Get the date when each tag in a repo was committed - this uses pagination, and nests API calls; getting rate limited is possible on a repository with many tags.
179
+
180
+ ```ruby
181
+ Nonacat.paginate_items("repos/list-tags", owner: 'notEthan', repo: 'nonacat').map do |tag|
182
+ {
183
+ name: tag.name,
184
+ # tag.commit includes very little; its `url` links to get the full commit resource
185
+ date: tag.commit.url.get.commit.committer.date,
186
+ }
187
+ end
188
+ ```
189
+
190
+ ## Development
191
+
192
+ - git clone
193
+ - `bin/nonacat_update` to fetch the latest Github OpenAPI document, if needed
194
+
195
+ ## License
196
+
197
+ The gem is available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/nonacat ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
4
+ require('nonacat')
5
+
6
+
7
+ # name Nonacat::<CATEGORY NAME>, Nonacat::<CATEGORY NAME>::<OPERATION NAME>, and Nonacat::<CATEGORY NAME>.<operation name> for irb tab-completable operation access
8
+
9
+ category_namespaces = Hash.new do |h, category|
10
+ category_namespace = Module.new { define_singleton_method(:category) { category } }
11
+ Nonacat.define_singleton_method(category.tr('-', '_')) { category_namespace }
12
+ Nonacat.const_set(category.tr('-', '_').upcase, category_namespace)
13
+ h[category] = category_namespace
14
+ end
15
+
16
+ Nonacat::GITHUB_API.operations.each do |operation|
17
+ category, name = operation.operationId.match(%r(\A([\w-]+)/([\w-]+)\z)).captures
18
+ category_namespaces[category].const_set(name.tr('-', '_').upcase, operation)
19
+ category_namespaces[category].define_singleton_method(name.tr('-', '_')) { operation }
20
+ end
21
+
22
+
23
+ proc do
24
+ # authorization from ~/.config/gh/hosts.yml, as used by gh-cli
25
+ gh_hosts_path = Pathname.new('~').expand_path.join('.config/gh/hosts.yml')
26
+ next unless gh_hosts_path.exist?
27
+ hosts = YAML.safe_load_file(gh_hosts_path)
28
+
29
+ # TODO not great
30
+ gh = hosts['github.com'] || next
31
+ token = gh['oauth_token'] || gh['users']&.values&.first&.[]('oauth_token') || next
32
+
33
+ Nonacat.authorization = ['Bearer', token]
34
+ end[]
35
+
36
+
37
+ require('irb')
38
+ IRB.start(__FILE__)
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Fetch Github's OpenAPI document from git ref ARGV[0] or the current `main` branch of https://github.com/github/rest-api-description
4
+ # For the gem, store the JSON compressed with zlib since, at 11 MB, it is rather excessive.
5
+ # For development, store the YAML for readability.
6
+
7
+ require('pathname')
8
+ require('fileutils')
9
+ require('net/http')
10
+ require('zlib')
11
+ require('json')
12
+ require('yaml')
13
+
14
+ api_dir = Pathname.new(__dir__).join('../github-rest-api-description')
15
+
16
+ def get_body(url)
17
+ response = Net::HTTP.get_response(URI(url))
18
+ raise("#{url} responded status: #{response.code}, body: #{response.body}") if !response.is_a?(Net::HTTPSuccess)
19
+ response.body
20
+ end
21
+
22
+ ref = ARGV[0] || 'main'
23
+ commit = JSON.parse(get_body("https://api.github.com/repos/github/rest-api-description/commits/#{ref}"))
24
+
25
+ at_latest = (api_dir / 'updated.yml').exist? && YAML.safe_load_file(api_dir / 'updated.yml')['sha'] == commit['sha']
26
+
27
+ # copy from github
28
+ ghcp = proc do |local_path, github_path, zz: false|
29
+ if at_latest && (api_dir / local_path).exist?
30
+ STDERR.puts("#{api_dir / local_path} already up to date at commit #{commit['sha']}; skipping")
31
+ next
32
+ end
33
+ STDERR.puts("fetching #{local_path}")
34
+ # getting this from the API is another option but it doesn't gzip so much slower
35
+ # at: "https://api.github.com/repos/github/rest-api-description/contents/#{github_path}?ref=#{commit['sha']}"
36
+ # with accept: 'application/vnd.github.raw'
37
+ url = "https://raw.githubusercontent.com/github/rest-api-description/#{commit['sha']}/#{github_path}"
38
+ FileUtils.mkdir_p((api_dir / local_path).dirname)
39
+ content = get_body(url)
40
+ content = Zlib.deflate(content) if zz
41
+ (api_dir / local_path).write(content)
42
+ end
43
+
44
+ ghcp['LICENSE.md', 'LICENSE.md']
45
+ ghcp['api.github.com.oas-3-0.json.zz', 'descriptions/api.github.com/api.github.com.json', zz: true]
46
+ ghcp['api.github.com.oas-3-1.json.zz', 'descriptions-next/api.github.com/api.github.com.json', zz: true]
47
+ # the YAML can be useful to browse for development but is excluded from the gem and git repo due to size
48
+ ghcp['api.github.com.oas-3-0.yaml', 'descriptions/api.github.com/api.github.com.yaml']
49
+ ghcp['api.github.com.oas-3-1.yaml', 'descriptions-next/api.github.com/api.github.com.yaml']
50
+
51
+ (api_dir / 'updated.yml').write(YAML.dump({
52
+ 'comment' => "The commit from which other files in this directory were updated (using bin/nonacat_update)",
53
+ 'sha' => commit['sha'],
54
+ 'date' => commit['commit']['committer']['date'],
55
+ }))
56
+
57
+ STDERR.puts("updated!")
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 GitHub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ ---
2
+ comment: The commit from which other files in this directory were updated (using bin/nonacat_update)
3
+ sha: 4ab8513682637010cd3bb5d8ee3227cc5ce739d1
4
+ date: '2024-12-06T13:11:37Z'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonacat
4
+ VERSION = "0.0.1"
5
+ end
data/lib/nonacat.rb ADDED
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("nonacat/version")
4
+ require("scorpio")
5
+ require("pathname")
6
+ require("zlib")
7
+ require("faraday/follow_redirects")
8
+
9
+ module Nonacat
10
+ # a module for JSI instances whose content is a URL corresponding to an operation of the OpenAPI description
11
+ module Link
12
+ # see [JSI::Base](https://rubydoc.info/gems/jsi/JSI/Base#jsi_as_child_default_as_jsi-instance_method)
13
+ def jsi_as_child_default_as_jsi
14
+ !jsi_node_content.nil?
15
+ end
16
+
17
+ def get(**conf, &b)
18
+ get_request(**conf, &b).run
19
+ end
20
+
21
+ # @return [Scorpio::Request]
22
+ def get_request(**conf, &b)
23
+ uri = JSI::URI[self]
24
+ conf[:query_params] = [uri.query_values, conf.delete(:query_params), conf.delete('query_params')].inject(nil) { |c, p| c ? p ? c.merge(p) : c : p }
25
+ GITHUB_API.operations.each do |operation|
26
+ next if !operation.get?
27
+ path_params = operation.uri_template.extract(operation.base_url.join(uri.merge(query: nil))) || next
28
+ conf[:path_params] = [conf.delete(:path_params), conf.delete('path_params')].inject(path_params) { |c, p| p ? c.merge(p) : c }
29
+ return operation.build_request(**conf, &b)
30
+ end
31
+ raise("no operation matched url: #{jsi_node_content}")
32
+ end
33
+ end
34
+
35
+ oass = ['3-0', '3-1']
36
+ oas = !ENV['NC_OAS'] ? oass.first : oass.include?(ENV['NC_OAS']) ? ENV['NC_OAS'] : abort("expected env NC_OAS in #{oass.join(', ')}")
37
+ GITHUB_API_PATH = Pathname.new(__dir__).join(-"../github-rest-api-description/api.github.com.oas-#{oas}.json.zz")
38
+
39
+ # A [Scorpio::OpenAPI::Document](https://rubydoc.info/gems/scorpio/Scorpio/OpenAPI/Document) for Github's API
40
+ GITHUB_API = Scorpio.new_document(
41
+ JSON.parse(Zlib.inflate(GITHUB_API_PATH.read)),
42
+ after_initialize: proc do |node|
43
+ # Name schema components like Github::CodeSearchResultItem, Github::Repository, etc.
44
+ if node.jsi_is_schema? && !node.jsi_schema_module_name
45
+ if node.jsi_ptr.parent == JSI::Ptr['components', 'schemas']
46
+ const_name = JSI::Util::Private.const_name_from_parts(node.jsi_ptr.tokens.last.to_s.split(/[_-]/)) # TODO shouldn't use JSI privates
47
+ Github.const_set(const_name, node.jsi_schema_module) if const_name && !Github.constants.include?(const_name.to_sym)
48
+ end
49
+ end
50
+
51
+ node_describes_url = node.jsi_is_schema? && (
52
+ node.keyword_value?('format', 'uri') ||
53
+ (node.keyword_value?('type', 'string') && (
54
+ (node.jsi_ptr.tokens.last.respond_to?(:to_str) && node.jsi_ptr.tokens.last =~ /_url\z/) ||
55
+ (node.example.respond_to?(:to_str) && node.example['://']))))
56
+ if node_describes_url
57
+ node.jsi_schema_module.include(Nonacat::Link)
58
+ end
59
+ end,
60
+ )
61
+
62
+ Github = GITHUB_API.jsi_schema_module_connection
63
+ module Github end
64
+
65
+ GITHUB_API.faraday_builder = proc do |conn|
66
+ conn.request(:authorization, *Nonacat.authorization) if Nonacat.authorization
67
+ conn.use(Faraday::FollowRedirects::Middleware)
68
+ end
69
+
70
+ class << self
71
+ # Authorization params passed to [Faraday::Request::Authorization](https://rubydoc.info/gems/faraday/Faraday/Request/Authorization).
72
+ #
73
+ # Nonacat.authorization = ['Bearer', 'github_pat_2kxqIkfByCRkCGT2...']
74
+ # Nonacat.authorization = [:basic, 'notEthan', 'p4$$w0rd']
75
+ attr_accessor(:authorization)
76
+
77
+ # Yields each item in each page of results from the indicated operation.
78
+ #
79
+ # @param operation [String, Scorpio::OpenAPI::Operation] an operationId or an Operation
80
+ # @param ratelimit [Boolean] {.ratelimit} each response
81
+ # @yield [JSI::Base] each item in each page of results
82
+ # @return [nil, Enumerator]
83
+ def paginate_items(operation, ratelimit: true, **conf, &block)
84
+ return to_enum(__method__, operation, **conf) unless block_given?
85
+ operation = operation.is_a?(Scorpio::OpenAPI::Operation) ? operation : GITHUB_API.operations[operation]
86
+ operation.each_link_page(**conf) do |page_ur|
87
+ if page_ur.response.body_object.respond_to?(:to_ary)
88
+ page_ur.response.body_object.each(&block)
89
+ elsif page_ur.response.body_object.respond_to?(:to_hash) && page_ur.response.body_object.key?('items') && page_ur.response.body_object['items'].respond_to?(:to_ary)
90
+ page_ur.response.body_object['items'].each(&block)
91
+ else
92
+ raise("pagination not detected in operation response.\noperation: #{operation.pretty_inspect.chomp}\nresponse ur: #{page_ur.pretty_inspect.chomp}")
93
+ end
94
+ Nonacat.ratelimit(page_ur) if ratelimit
95
+ end
96
+ end
97
+
98
+ # If the given ur's response indicates insufficent remaining ratelimit, sleep until limit will reset
99
+ def ratelimit(ur)
100
+ if ur.response.headers['x-ratelimit-remaining'] && Float(ur.response.headers['x-ratelimit-remaining']) <= 1 && ur.response.headers['x-ratelimit-reset']
101
+ sleep(1 + (Time.at(Float(ur.response.headers['x-ratelimit-reset'])) - Time.now))
102
+ end
103
+ ur
104
+ end
105
+ end
106
+ end
data/nonacat.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/nonacat/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "nonacat"
7
+ spec.version = Nonacat::VERSION
8
+ spec.authors = ["Ethan"]
9
+ spec.email = ["ethan@unth.net"]
10
+
11
+ spec.summary = "A Github API client - unofficial, unaffiliated"
12
+ spec.description = "Nonacat builds from Github's OpenAPI description using the gem `scorpio` to offer an alternative client to Github's REST API."
13
+ spec.homepage = "https://github.com/notEthan/nonacat"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = [
17
+ 'LICENSE.txt',
18
+ 'README.md',
19
+ '.yardopts',
20
+ 'nonacat.gemspec',
21
+ *Dir['lib/**/*'],
22
+ 'github-rest-api-description/LICENSE.md',
23
+ *Dir['github-rest-api-description/api.github.com.*.json.zz'],
24
+ 'github-rest-api-description/updated.yml',
25
+ ].reject { |f| File.lstat(f).ftype == 'directory' }
26
+
27
+ spec.require_paths = ["lib"]
28
+ spec.bindir = 'bin'
29
+ spec.executables = ['nonacat', 'nonacat_update']
30
+
31
+ spec.add_dependency('scorpio', '~> 0.7')
32
+ spec.add_dependency('faraday-follow_redirects')
33
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nonacat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ethan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: scorpio
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-follow_redirects
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Nonacat builds from Github's OpenAPI description using the gem `scorpio`
42
+ to offer an alternative client to Github's REST API.
43
+ email:
44
+ - ethan@unth.net
45
+ executables:
46
+ - nonacat
47
+ - nonacat_update
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - ".yardopts"
52
+ - LICENSE.txt
53
+ - README.md
54
+ - bin/nonacat
55
+ - bin/nonacat_update
56
+ - github-rest-api-description/LICENSE.md
57
+ - github-rest-api-description/api.github.com.oas-3-0.json.zz
58
+ - github-rest-api-description/api.github.com.oas-3-1.json.zz
59
+ - github-rest-api-description/updated.yml
60
+ - lib/nonacat.rb
61
+ - lib/nonacat/version.rb
62
+ - nonacat.gemspec
63
+ homepage: https://github.com/notEthan/nonacat
64
+ licenses:
65
+ - MIT
66
+ metadata: {}
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.5.22
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: A Github API client - unofficial, unaffiliated
86
+ test_files: []