agoo 2.15.10 → 2.15.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +20 -7
- data/ext/agoo/agoo.c +2 -2
- data/ext/agoo/debug.c +2 -2
- data/ext/agoo/domain.c +2 -2
- data/ext/agoo/dtime.c +1 -1
- data/ext/agoo/error_stream.c +1 -1
- data/ext/agoo/gqleval.c +1 -1
- data/ext/agoo/graphql.c +1 -1
- data/ext/agoo/http.c +2 -2
- data/ext/agoo/log.c +9 -9
- data/ext/agoo/page.c +506 -506
- data/ext/agoo/rack_logger.c +1 -1
- data/ext/agoo/rresponse.c +1 -1
- data/ext/agoo/rserver.c +31 -16
- data/lib/agoo/version.rb +1 -1
- data/misc/flymd.md +581 -0
- data/misc/glue-diagram.svg +3 -0
- data/misc/glue.md +25 -0
- data/misc/optimize.md +100 -0
- data/misc/push.md +581 -0
- data/misc/rails.md +174 -0
- data/misc/song.md +268 -0
- metadata +18 -2
data/misc/rails.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# Rails with Agoo
|
2
|
+
|
3
|
+
Agoo gives Rails a performance boost. It's easy to use Agoo with Rails. Start
|
4
|
+
with a Rails project. Something simple.
|
5
|
+
|
6
|
+
```
|
7
|
+
$ rails new blog
|
8
|
+
$ rails g scaffold User
|
9
|
+
$ rails db:migrate
|
10
|
+
```
|
11
|
+
|
12
|
+
Now run it with Agoo.
|
13
|
+
|
14
|
+
```
|
15
|
+
$ rackup -r agoo -s agoo
|
16
|
+
```
|
17
|
+
|
18
|
+
Or for an extra boost on a machine with multiple cores fire up Agoo with
|
19
|
+
multiple workers. The optimium seems to be about one worker per hyperthread or
|
20
|
+
two per core.
|
21
|
+
|
22
|
+
```
|
23
|
+
$ rackup -r agoo -s agoo -O wc=12
|
24
|
+
```
|
25
|
+
|
26
|
+
What you should see is a faster Rails. Requests to something like
|
27
|
+
`http://localhost:9292/users/1` should be several times faster and asset
|
28
|
+
fetches should be 8000 times faster. Here are some benchmark runs using the
|
29
|
+
[perfer](https://github.com/ohler55/perfer) benchmark tool.
|
30
|
+
|
31
|
+
It is easy to push Rails too hard and watch the latency climb. To avoid that
|
32
|
+
the number of open connections use is adjusted according to how well the
|
33
|
+
server and Rails can handle the load. A balance where the latency is as close
|
34
|
+
to the best attainable is used and the throughput is maximized for that
|
35
|
+
latency is used in all cases.
|
36
|
+
|
37
|
+
All benchmarks were run on a 6 core i7-8700 system with 16GB of memory. The
|
38
|
+
driver, `perfer` was run on a separate machine to allow Rails and the web
|
39
|
+
server full use of the benchmark machine.
|
40
|
+
|
41
|
+
### With Rails managed objects
|
42
|
+
|
43
|
+
For the benchmarks a blank `User` object is created using a browser. The a
|
44
|
+
fetch is performed repeatedly on that object.
|
45
|
+
|
46
|
+
##### With Rails and the default Puma server.
|
47
|
+
|
48
|
+
```
|
49
|
+
$ perfer -t 1 -c 1 -b 1 192.168.1.11:9292 -p /users/1 -d 10
|
50
|
+
Benchmarks for:
|
51
|
+
URL: 192.168.1.11:9292/users/1
|
52
|
+
Threads: 1
|
53
|
+
Connections/thread: 1
|
54
|
+
Duration: 10.0 seconds
|
55
|
+
Keep-Alive: false
|
56
|
+
Results:
|
57
|
+
Throughput: 41 requests/second
|
58
|
+
Latency: 23.612 +/-5.111 msecs (and stdev)
|
59
|
+
```
|
60
|
+
|
61
|
+
##### Now the same request with Agoo in non-clustered mode.
|
62
|
+
|
63
|
+
```
|
64
|
+
$ perfer -t 2 -k -c 4 -b 1 192.168.1.11:9292 -p /users/1 -d 10
|
65
|
+
Benchmarks for:
|
66
|
+
URL: 192.168.1.11:9292/users/1
|
67
|
+
Threads: 2
|
68
|
+
Connections/thread: 4
|
69
|
+
Duration: 5.0 seconds
|
70
|
+
Keep-Alive: true
|
71
|
+
Results:
|
72
|
+
Throughput: 279 requests/second
|
73
|
+
Latency: 28.608 +/-3.497 msecs (and stdev)
|
74
|
+
```
|
75
|
+
|
76
|
+
Agoo was able to handle 8 concurrent connections and still maintain the
|
77
|
+
latency target of under 30 millisecond. Throughput with Agoo was also 7 times
|
78
|
+
greater. Most of the time spent with both Agoo and Puma is most likely in
|
79
|
+
Rails and Rackup so lets try Agoo in cluster mode with the application is
|
80
|
+
stateless.
|
81
|
+
|
82
|
+
##### Now the same request with Agoo in clustered mode.
|
83
|
+
|
84
|
+
```
|
85
|
+
$ perfer -t 2 -k -c 20 -b 1 192.168.1.11:9292 -p /users/1 -d 10
|
86
|
+
Benchmarks for:
|
87
|
+
URL: 192.168.1.11:9292/users/1
|
88
|
+
Threads: 2
|
89
|
+
Connections/thread: 20
|
90
|
+
Duration: 5.0 seconds
|
91
|
+
Keep-Alive: true
|
92
|
+
Results:
|
93
|
+
Throughput: 1476 requests/second
|
94
|
+
Latency: 27.051 +/-11.107 msecs (and stdev)
|
95
|
+
```
|
96
|
+
|
97
|
+
Another 5x boost in throughput. That make Agoo in cluster mode 36 times faster
|
98
|
+
than the default Puma. In all fairness, Puma can be put into clustered mode as
|
99
|
+
well using a custom configuration file. If anyone would like to provide
|
100
|
+
formula for running Puma at optimum please let me know or create a PR for how
|
101
|
+
to run it more effectively.
|
102
|
+
|
103
|
+
### Fetching static assets
|
104
|
+
|
105
|
+
Rails likes to be in charge and is responsible for serving static assets. Some
|
106
|
+
servers such as Agoo offer an option to bypass Rails handling of static
|
107
|
+
assets. Allowing static assets to be loaded directly means CSS, HTML, images,
|
108
|
+
Javascript, and other static files load quickly so that web sites are more
|
109
|
+
snappy even when more than a handful of users are connected.
|
110
|
+
|
111
|
+
##### Rails with the default Puma server loading a static asset.
|
112
|
+
```
|
113
|
+
$ perfer -t 2 -k -c 1 -b 1 192.168.1.11:9292 -p /robots.txt -d 10
|
114
|
+
Benchmarks for:
|
115
|
+
URL: 192.168.1.11:9292/robots.txt
|
116
|
+
Threads: 2
|
117
|
+
Connections/thread: 1
|
118
|
+
Duration: 10.0 seconds
|
119
|
+
Keep-Alive: true
|
120
|
+
Results:
|
121
|
+
Throughput: 79 requests/second
|
122
|
+
Latency: 25.027 +/-7.800 msecs (and stdev)
|
123
|
+
```
|
124
|
+
|
125
|
+
Well, Rails with Puma handles almost twice as as many request as it does for
|
126
|
+
Ruby processing of managed object calls. Note that Puma was able to handle 2
|
127
|
+
concurrent connections without degradation of the latency.
|
128
|
+
|
129
|
+
##### Rails with Agoo loading a static asset in non-clustered mode.
|
130
|
+
```
|
131
|
+
$ perfer -t 2 -k -c 40 -b 2 192.168.1.11:9292 -p /robots.txt -d 10
|
132
|
+
Benchmarks for:
|
133
|
+
URL: 192.168.1.11:9292/robots.txt
|
134
|
+
Threads: 2
|
135
|
+
Connections/thread: 40
|
136
|
+
Duration: 5.0 seconds
|
137
|
+
Keep-Alive: true
|
138
|
+
Results:
|
139
|
+
Throughput: 500527 requests/second
|
140
|
+
Latency: 0.292 +/-0.061 msecs (and stdev)
|
141
|
+
```
|
142
|
+
|
143
|
+
Wow, more than 6000 times faster and the latency drops from 25 milliseconds to
|
144
|
+
a fraction of a millisecond. There is a reason for that. Agoo looks at the
|
145
|
+
`Rails.configuration.assets.paths` variable and sets up a bypass to load those
|
146
|
+
files directly using the same approach as Rails. Ruby is no longer has to deal
|
147
|
+
with basic file serving but can be relogated to taking care of business. It
|
148
|
+
also means tha no Ruby objects are created for serving static assets.
|
149
|
+
|
150
|
+
Now how about Agoo in clustered mode.
|
151
|
+
|
152
|
+
##### Rails with Agoo loading a static asset in clustered mode.
|
153
|
+
```
|
154
|
+
$ perfer -t 2 -k -c 40 -b 2 192.168.1.11:9292 -p /robots.txt -d 10
|
155
|
+
Benchmarks for:
|
156
|
+
URL: 192.168.1.11:9292/robots.txt
|
157
|
+
Threads: 2
|
158
|
+
Connections/thread: 40
|
159
|
+
Duration: 5.0 seconds
|
160
|
+
Keep-Alive: true
|
161
|
+
Results:
|
162
|
+
Throughput: 657777 requests/second
|
163
|
+
Latency: 0.223 +/-0.073 msecs (and stdev)
|
164
|
+
```
|
165
|
+
|
166
|
+
A bit faster but not that much over the non-clustered Agoo. The limiting
|
167
|
+
factor with static assets is the network. Agoo handles 80 concurrent
|
168
|
+
connections at the same latency as one.
|
169
|
+
|
170
|
+
### Summary
|
171
|
+
|
172
|
+
Benchmarks are not everything but if they are an important consideration when
|
173
|
+
selecting a web server for Rails or Rack then Agoo clearly has an advantage
|
174
|
+
over the Rack and Rails default.
|
data/misc/song.md
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
# GraphQL with a Song
|
2
|
+
|
3
|
+
You've may have heard developer singing praises about how wonderful GraphQL
|
4
|
+
is. Maybe you thought it was looking into. I like to learn new technologies by
|
5
|
+
using them so this article is a simple example of using GraphQL.
|
6
|
+
|
7
|
+
GraphQL is a language for describing an applicaiton API. It holds a similar
|
8
|
+
position in the development stack as a REST API but with more
|
9
|
+
flexibility. Unlike REST, GraphQL allows response formats and content to be
|
10
|
+
specified by the client. Just as SQL `SELECT` statements allow query results
|
11
|
+
to be specified, GraphQL allows returned JSON data structure to be
|
12
|
+
specified. Following the SQL analogy GraphQL does not provide a `WHERE` clause
|
13
|
+
but identifies fields on application objects that should provide the data for
|
14
|
+
the response.
|
15
|
+
|
16
|
+
GraphQL, as the name suggests models APIs as if the application is a graph of
|
17
|
+
data. While that description may not be how you have viewed your application
|
18
|
+
it is a model used in most systems. If your application is object based then
|
19
|
+
the objects refer to other objects. These object-method-object define a
|
20
|
+
graph. Data that can be represented by JSON is a graph as JSON is just a
|
21
|
+
directed graph. Thinking about the application as presenting a graph model
|
22
|
+
through the API will make GraphQL much easier to understand.
|
23
|
+
|
24
|
+
## Consider the Application
|
25
|
+
|
26
|
+
Enough of the abstract. Lets get down to actually building an application that
|
27
|
+
uses GraphQL by starting with a definition of the data model or the
|
28
|
+
graph. Last year I picked up a new hobby. I'm learning to play the electric
|
29
|
+
upright bass and about music so a music related example came to mind when
|
30
|
+
coming up with and example.
|
31
|
+
|
32
|
+
Keeping it simple the object types are __Artist__ and __Song__. __Artists__
|
33
|
+
have multiple __Song__ and a __Song__ is associate with an __Artist__. Each
|
34
|
+
object type has attributes such as a `name`. Pretty basic and simple.
|
35
|
+
|
36
|
+
## Define the API
|
37
|
+
|
38
|
+
GraphQL uses SDL (Schema Definition Language) which is sometimes refered to as
|
39
|
+
"type system definition language" in the GraphQL specification. GraphQL types
|
40
|
+
can, in theory be defined in any language but most common language agnostic
|
41
|
+
language is SDL so lets use SDL to define the API.
|
42
|
+
|
43
|
+
```
|
44
|
+
type Artist {
|
45
|
+
name: String!
|
46
|
+
songs: [Song]
|
47
|
+
origin: [String]
|
48
|
+
}
|
49
|
+
|
50
|
+
type Song {
|
51
|
+
name: String!
|
52
|
+
artist: Artist
|
53
|
+
duration: Int
|
54
|
+
release: String
|
55
|
+
}
|
56
|
+
```
|
57
|
+
|
58
|
+
Pretty easy to understand. An __Artist__ has a `name` that is a `String`,
|
59
|
+
`songs` that is an array of __Song__ objects, and `origin` which is a `String`
|
60
|
+
array. __Song__ is similar but with one odd field. The `release` field should
|
61
|
+
be a time or date type but GraphQL does not have that type defined as a core
|
62
|
+
type. To be completely portable between any GraphQL implementation a `String`
|
63
|
+
is used. The GraphQL implemenation we will use as added the `Time` type so
|
64
|
+
lets change the __Song__ definition so that the `release` field is a `Time`
|
65
|
+
type. The returned value will be a `String` but by setting the type to `Time`
|
66
|
+
we document the API more accurately.
|
67
|
+
|
68
|
+
```
|
69
|
+
release: Time
|
70
|
+
```
|
71
|
+
|
72
|
+
The last step is to describe how to get one or more of the objects. This is
|
73
|
+
referred to as the root or for queries the query root. Our root will have just
|
74
|
+
one field or method called `artist` and will require an artist `name`.
|
75
|
+
|
76
|
+
```
|
77
|
+
type Query {
|
78
|
+
artist(name: String!): Artist
|
79
|
+
}
|
80
|
+
```
|
81
|
+
|
82
|
+
## Writing the Application
|
83
|
+
|
84
|
+
Ruby will be used write the application. There is more than one implemenation
|
85
|
+
of a GraphQL server for Ruby. Some approachs require the SDL above to be
|
86
|
+
translated into a Ruby equivalent. [Agoo](https://github.com/ohler55/agoo)
|
87
|
+
uses the SDL defintion as it is and the Ruby code is plain vanilla Ruby so
|
88
|
+
that will be used for this example.
|
89
|
+
|
90
|
+
By having the Ruby class names match the GraphQL type names we keep things
|
91
|
+
simple. Note that the Ruby classes match the GraphQL types.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class Artist
|
95
|
+
attr_reader :name
|
96
|
+
attr_reader :songs
|
97
|
+
attr_reader :origin
|
98
|
+
|
99
|
+
def initialize(name, origin)
|
100
|
+
@name = name
|
101
|
+
@songs = []
|
102
|
+
@origin = origin
|
103
|
+
end
|
104
|
+
|
105
|
+
def song(args={})
|
106
|
+
@songs[args['name']]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Only used by the Song to add itself to the artist.
|
110
|
+
def add_song(song)
|
111
|
+
@songs << song
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Song
|
116
|
+
attr_reader :name # string
|
117
|
+
attr_reader :artist # reference
|
118
|
+
attr_reader :duration # integer
|
119
|
+
attr_reader :release # time
|
120
|
+
|
121
|
+
def initialize(name, artist, duration, release)
|
122
|
+
@name = name
|
123
|
+
@artist = artist
|
124
|
+
@duration = duration
|
125
|
+
@release = release
|
126
|
+
artist.add_song(self)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
Method match fields in the Ruby classes. Note the method all have
|
132
|
+
either no arguments or `args={}`. This is what the GraphQL APIs expect
|
133
|
+
and the [Agoo](https://github.com/ohler55/agoo) GraphQL implementation
|
134
|
+
follows suit. [Agoo](https://github.com/ohler55/agoo) offers
|
135
|
+
additional options that are available based on the method
|
136
|
+
signature. If a method signature is `(args, req)` or `(args, req,
|
137
|
+
plan)` then the request object is included. The plan, if included, is
|
138
|
+
an object that can be queried for information about the query. (It has
|
139
|
+
not been implemented as of version 2.12.0.)
|
140
|
+
|
141
|
+
The `initialize` methods are used to set up the data for this example
|
142
|
+
as we will see shortly.
|
143
|
+
|
144
|
+
The query root class als needs to be defined. Note the `artist` method which
|
145
|
+
matches the SDL `Query` root type. An `attr_reader` for `artists` was also
|
146
|
+
added. That would be exposed to the API simply by adding that field to the
|
147
|
+
`Query` type in the SDL document.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class Query
|
151
|
+
attr_reader :artists
|
152
|
+
|
153
|
+
def initialize(artists)
|
154
|
+
@artists = artists
|
155
|
+
end
|
156
|
+
|
157
|
+
def artist(args={})
|
158
|
+
@artists[args['name']]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
The GraphQL root, not to be confused with the query root sites above the query
|
164
|
+
root. GraphQL defines it to optionally have three field. The Ruby class in
|
165
|
+
this case implements on the `query` field. The initializer loads up some data
|
166
|
+
for an Indie band from New Zealand I like to listen to.
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class Schema
|
170
|
+
attr_reader :query
|
171
|
+
attr_reader :mutation
|
172
|
+
attr_reader :subscription
|
173
|
+
|
174
|
+
def initialize()
|
175
|
+
# Set up some data for testing.
|
176
|
+
artist = Artist.new('Fazerdaze', ['Morningside', 'Auckland', 'New Zealand'])
|
177
|
+
Song.new('Jennifer', artist, 240, Time.utc(2017, 5, 5))
|
178
|
+
Song.new('Lucky Girl', artist, 170, Time.utc(2017, 5, 5))
|
179
|
+
Song.new('Friends', artist, 194, Time.utc(2017, 5, 5))
|
180
|
+
Song.new('Reel', artist, 193, Time.utc(2015, 11, 2))
|
181
|
+
@artists = {artist.name => artist}
|
182
|
+
|
183
|
+
@query = Query.new(@artists)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
The final hookup is implemenation specific. For
|
189
|
+
[Agoo](https://github.com/ohler55/agoo) the server is initialized to include a
|
190
|
+
handler for the `/graphql` HTTP request path ad then it is started.
|
191
|
+
|
192
|
+
```
|
193
|
+
Agoo::Server.init(6464, 'root', thread_count: 1, graphql: '/graphql')
|
194
|
+
Agoo::Server.start()
|
195
|
+
```
|
196
|
+
|
197
|
+
The GraphQL implementation is then configured with the SDL defined earlier
|
198
|
+
(`$songs_sdl`) and then the appliation sleeps while the server processes
|
199
|
+
requests.
|
200
|
+
|
201
|
+
```
|
202
|
+
Agoo::GraphQL.schema(Schema.new) {
|
203
|
+
Agoo::GraphQL.load($songs_sdl)
|
204
|
+
}
|
205
|
+
sleep
|
206
|
+
```
|
207
|
+
|
208
|
+
The code for this example is in (https://github.com/ohler55/agoo/example/graphql/song.rb).
|
209
|
+
|
210
|
+
## Using the API
|
211
|
+
|
212
|
+
A web broswer can be used to try out the API as can `curl`.
|
213
|
+
|
214
|
+
The GraphQL query to try looks like the following.
|
215
|
+
|
216
|
+
```
|
217
|
+
{
|
218
|
+
artist(name:"Fazerdaze") {
|
219
|
+
name
|
220
|
+
songs{
|
221
|
+
name
|
222
|
+
duration
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
```
|
227
|
+
|
228
|
+
The query asks for the __Artist__ named `Frazerdaze` and returns the `name` and `songs` in a JSON document. For each __Song__ the `name` and `duration` of the __Song__ is returned in a JSON object for each __Song__. The output should look like this.
|
229
|
+
|
230
|
+
```
|
231
|
+
{
|
232
|
+
"data":{
|
233
|
+
"artist":{
|
234
|
+
"name":"Fazerdaze",
|
235
|
+
"songs":[
|
236
|
+
{
|
237
|
+
"name":"Jennifer",
|
238
|
+
"duration":240
|
239
|
+
},
|
240
|
+
{
|
241
|
+
"name":"Lucky Girl",
|
242
|
+
"duration":170
|
243
|
+
},
|
244
|
+
{
|
245
|
+
"name":"Friends",
|
246
|
+
"duration":194
|
247
|
+
},
|
248
|
+
{
|
249
|
+
"name":"Reel",
|
250
|
+
"duration":193
|
251
|
+
}
|
252
|
+
]
|
253
|
+
}
|
254
|
+
}
|
255
|
+
}
|
256
|
+
```
|
257
|
+
|
258
|
+
After getting rid of the optional whitespace in the query an HTTP GET made
|
259
|
+
with curl should return that content.
|
260
|
+
|
261
|
+
```
|
262
|
+
curl -w "\n" 'localhost:6464/graphql?query=\{artist(name:"Fazerdaze")\{name,songs\{name,duration\}\}\}&indent=2'
|
263
|
+
```
|
264
|
+
|
265
|
+
Try changing the query and replace `duration` with `release` and not the
|
266
|
+
conversion of the Ruby Time to a JSON string.
|
267
|
+
|
268
|
+
Hope you enjoyed the example. Now you can sing the GraphQL song.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: agoo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.15.
|
4
|
+
version: 2.15.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Ohler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -41,6 +41,13 @@ extra_rdoc_files:
|
|
41
41
|
- README.md
|
42
42
|
- CHANGELOG.md
|
43
43
|
- LICENSE
|
44
|
+
- misc/flymd.md
|
45
|
+
- misc/glue.md
|
46
|
+
- misc/optimize.md
|
47
|
+
- misc/push.md
|
48
|
+
- misc/rails.md
|
49
|
+
- misc/song.md
|
50
|
+
- misc/glue-diagram.svg
|
44
51
|
files:
|
45
52
|
- CHANGELOG.md
|
46
53
|
- LICENSE
|
@@ -150,6 +157,13 @@ files:
|
|
150
157
|
- lib/agoo/graphql/type.rb
|
151
158
|
- lib/agoo/version.rb
|
152
159
|
- lib/rack/handler/agoo.rb
|
160
|
+
- misc/flymd.md
|
161
|
+
- misc/glue-diagram.svg
|
162
|
+
- misc/glue.md
|
163
|
+
- misc/optimize.md
|
164
|
+
- misc/push.md
|
165
|
+
- misc/rails.md
|
166
|
+
- misc/song.md
|
153
167
|
- test/base_handler_test.rb
|
154
168
|
- test/bind_test.rb
|
155
169
|
- test/domain_test.rb
|
@@ -164,7 +178,9 @@ homepage: https://github.com/ohler55/agoo
|
|
164
178
|
licenses:
|
165
179
|
- MIT
|
166
180
|
metadata:
|
181
|
+
bug_tracker_uri: https://github.com/ohler55/agoo/issues
|
167
182
|
changelog_uri: https://github.com/ohler55/agoo/CHANGELOG.md
|
183
|
+
documentation_uri: http://www.ohler.com/agoo/index.html
|
168
184
|
source_code_uri: https://github.com/ohler55/agoo
|
169
185
|
homepage: https://github.com/ohler55/agoo
|
170
186
|
post_install_message:
|