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 +7 -0
- data/.yardopts +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +197 -0
- data/bin/nonacat +38 -0
- data/bin/nonacat_update +57 -0
- data/github-rest-api-description/LICENSE.md +21 -0
- data/github-rest-api-description/api.github.com.oas-3-0.json.zz +0 -0
- data/github-rest-api-description/api.github.com.oas-3-1.json.zz +0 -0
- data/github-rest-api-description/updated.yml +4 -0
- data/lib/nonacat/version.rb +5 -0
- data/lib/nonacat.rb +106 -0
- data/nonacat.gemspec +33 -0
- metadata +86 -0
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
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__)
|
data/bin/nonacat_update
ADDED
|
@@ -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.
|
|
Binary file
|
|
Binary file
|
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: []
|