statsrb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ require 'mkmf'
2
+
3
+ create_makefile('statsrb/statsrb')
@@ -0,0 +1,450 @@
1
+ #include <ruby.h>
2
+ #include <stdio.h>
3
+ #include <string.h>
4
+ #include <stdlib.h>
5
+
6
+ /**
7
+ * Loads a file and filters on a specified namespace.
8
+ */
9
+ static VALUE statsrb_query(VALUE self, VALUE logfile, VALUE query_ns, VALUE query_limit, VALUE query_start, VALUE query_end) {
10
+ FILE * file;
11
+ int line_size = 256;
12
+ char *line = (char *) malloc(line_size);
13
+ const char *filepath = RSTRING_PTR(logfile);
14
+ const char *query_ns_char = RSTRING_PTR(query_ns);
15
+
16
+ // @data hash key symbols.
17
+ VALUE statsrb_key_ts = rb_iv_get(self, "@key_ts");
18
+ VALUE statsrb_key_ns = rb_iv_get(self, "@key_ns");
19
+ VALUE statsrb_key_v = rb_iv_get(self, "@key_v");
20
+ // Create an empty string for comparison.
21
+ VALUE statsrb_str_empty = rb_str_new2("");
22
+
23
+ // Convert into an int that ruby understands.
24
+ int limit = NUM2INT(query_limit);
25
+ int qstart = NUM2INT(query_start);
26
+ int qend = NUM2INT(query_end);
27
+
28
+ // Return array instantiation.
29
+ VALUE statsrb_data = rb_iv_get(self, "@data");
30
+ // @TODO does this garbage collect all of the old hash data?
31
+ rb_ary_resize(statsrb_data, 0);
32
+
33
+ file = fopen(filepath, "r");
34
+ if (file == NULL) {
35
+ fprintf(stderr, "File error: could not open file %s for reading.", filepath);
36
+ return;
37
+ }
38
+
39
+ int count = 0;
40
+
41
+ while (NULL != fgets(line, line_size, file) && count < limit) {
42
+ // strstr doesn't work with newline chars.
43
+ size_t len = strlen(line) - 1;
44
+ if (line[len] == '\n');
45
+ line[len] = '\0';
46
+
47
+ // If the namespace is in the row, explode it.
48
+ if (line[0] != '\0' && line[0] != '\n' && strchr(line, query_ns_char[0]) && strstr(line, query_ns_char)) {
49
+ VALUE statsrb_event = rb_hash_new();
50
+
51
+ // I tried sscanf for convenience, but it was predictably slower.
52
+ //int statsrb_ts, statsrb_v;
53
+ //sscanf(line, "%d\t%*s\t%d", &statsrb_ts, &statsrb_v);
54
+
55
+ // @TODO this should something more robust than atoi.
56
+ int statsrb_ts = atoi(strtok(line, "\t"));
57
+
58
+ if (statsrb_ts != NULL && (qstart == 0 || statsrb_ts >= qstart) && (qend == 0 || statsrb_ts <= qend)) {
59
+ // @TODO this should probably use the actual namespace if we do wildcard queries.
60
+ VALUE statsrb_str_ns = rb_str_new2(strtok(NULL, "\t"));
61
+ //strtok(NULL, "\t");
62
+ int statsrb_v = atoi(strtok(NULL, "\0"));
63
+
64
+ // @TODO this should really query the namespace exactly instead of just relying on strstr.
65
+ //if (rb_str_cmp(query_ns, statsrb_str_empty) == 0 || rb_str_cmp(query_ns, statsrb_str_ns) == 0) {
66
+ if (statsrb_ts && (statsrb_v || statsrb_v == 0)) {
67
+ rb_hash_aset(statsrb_event, statsrb_key_ts, INT2NUM(statsrb_ts));
68
+ rb_hash_aset(statsrb_event, statsrb_key_ns, statsrb_str_ns);
69
+ //rb_hash_aset(statsrb_event, statsrb_key_ns, query_ns);
70
+ rb_hash_aset(statsrb_event, statsrb_key_v, INT2NUM(statsrb_v));
71
+ rb_ary_push(statsrb_data, statsrb_event);
72
+ count++;
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ // terminate
79
+ fclose (file);
80
+ free (line);
81
+
82
+ //return statsrb_data;
83
+ //rb_iv_set(self, "@data", statsrb_data);
84
+
85
+ return self;
86
+ }
87
+
88
+ /**
89
+ * Implementation of quicksort algorithm.
90
+ */
91
+ void time_sort(int left, int right, VALUE ary, VALUE statsrb_key_ts) {
92
+ int i = left;
93
+ int j = right;
94
+ int p = (i + j) / 2;
95
+ int pv = NUM2INT(rb_hash_aref(rb_ary_entry(ary, p), statsrb_key_ts));
96
+ VALUE tmp;
97
+
98
+ while (i <= j) {
99
+ while (NUM2INT(rb_hash_aref(rb_ary_entry(ary, i), statsrb_key_ts)) < pv) {
100
+ i++;
101
+ }
102
+ while (NUM2INT(rb_hash_aref(rb_ary_entry(ary, j), statsrb_key_ts)) > pv) {
103
+ j--;
104
+ }
105
+ if (i <= j) {
106
+ tmp = rb_ary_entry(ary, i);
107
+ rb_ary_store(ary, i, rb_ary_entry(ary, j));
108
+ rb_ary_store(ary, j, tmp);
109
+ i++;
110
+ j--;
111
+ }
112
+ }
113
+
114
+ if (left < j) {
115
+ time_sort(left, j, ary, statsrb_key_ts);
116
+ }
117
+ if (i < right) {
118
+ time_sort(i, right, ary, statsrb_key_ts);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Sort the internal data using a quicksort algorithm based on the hash element's timestamp.
124
+ */
125
+ static VALUE statsrb_sort(VALUE self) {
126
+ VALUE statsrb_data = rb_iv_get(self, "@data");
127
+ int len = RARRAY_LEN(statsrb_data);
128
+ if (len > 0) {
129
+ VALUE statsrb_key_ts = rb_iv_get(self, "@key_ts");
130
+ time_sort(0, len - 1, statsrb_data, statsrb_key_ts);
131
+ }
132
+ return statsrb_data;
133
+ }
134
+
135
+ /**
136
+ * Write the in-memory data to a file.
137
+ */
138
+ static VALUE statsrb_write(VALUE self, VALUE logfile, VALUE mode) {
139
+ FILE * file;
140
+ const char *filepath = RSTRING_PTR(logfile);
141
+ const char *filemode = RSTRING_PTR(mode);
142
+ VALUE statsrb_data = rb_iv_get(self, "@data");
143
+ int data_length = RARRAY_LEN(statsrb_data);
144
+ int i;
145
+ int line_size = 256;
146
+ int tmp_ts, tmp_v;
147
+ const char *tmp_ns = (char *) malloc(line_size);
148
+
149
+ // @data hash key symbols.
150
+ VALUE statsrb_key_ts = rb_iv_get(self, "@key_ts");
151
+ VALUE statsrb_key_ns = rb_iv_get(self, "@key_ns");
152
+ VALUE statsrb_key_v = rb_iv_get(self, "@key_v");
153
+
154
+ file = fopen(filepath, filemode);
155
+ if (file==NULL) {
156
+ fprintf(stderr, "File error: could not open file %s mode %s.", filepath, filemode);
157
+ return;
158
+ }
159
+
160
+ // Iterate through the data array, writing the data as we go.
161
+ for (i = 0; i < data_length; i++) {
162
+ // @TODO make sure that these values are not empty before writing.
163
+ //VALUE tmp_line = rb_str_tmp_new(line_size);
164
+ tmp_ts = NUM2INT(rb_hash_aref(rb_ary_entry(statsrb_data, i), statsrb_key_ts));
165
+ tmp_ns = RSTRING_PTR(rb_hash_aref(rb_ary_entry(statsrb_data, i), statsrb_key_ns));
166
+ tmp_v = NUM2INT(rb_hash_aref(rb_ary_entry(statsrb_data, i), statsrb_key_v));
167
+ fprintf(file, "%d\t%s\t%d\n", tmp_ts, tmp_ns, tmp_v);
168
+ //rb_str_free(tmp_line);
169
+ }
170
+
171
+ fclose (file);
172
+ return self;
173
+ }
174
+
175
+ /**
176
+ * A method to split unique namespaces from internal memory and write them to individual files.
177
+ */
178
+ static VALUE statsrb_split_write(VALUE self, VALUE logdir, VALUE mode) {
179
+ VALUE statsrb_data = rb_iv_get(self, "@data");
180
+ int len = RARRAY_LEN(statsrb_data);
181
+ int i, ii, ns_len;
182
+
183
+ // @data hash key symbols.
184
+ VALUE statsrb_key_ts = rb_iv_get(self, "@key_ts");
185
+ VALUE statsrb_key_ns = rb_iv_get(self, "@key_ns");
186
+ VALUE statsrb_key_v = rb_iv_get(self, "@key_v");
187
+
188
+ VALUE ns_list = rb_ary_new();
189
+
190
+ for (i = 0; i < len; i++) {
191
+ if (!rb_ary_includes(ns_list, rb_hash_aref(rb_ary_entry(statsrb_data, i), statsrb_key_ns))) {
192
+ rb_ary_push(ns_list, rb_hash_aref(rb_ary_entry(statsrb_data, i), statsrb_key_ns));
193
+ }
194
+ }
195
+
196
+ ns_len = RARRAY_LEN(ns_list);
197
+
198
+ for (i = 0; i < ns_len; i++) {
199
+ VALUE tmp = rb_obj_dup(self);
200
+ VALUE tmp_data = rb_ary_new();
201
+ for (ii = 0; ii < len; ii++) {
202
+ if (rb_str_cmp(rb_ary_entry(ns_list, i), rb_hash_aref(rb_ary_entry(statsrb_data, ii), statsrb_key_ns)) == 0) {
203
+ rb_ary_push(tmp_data, rb_ary_entry(statsrb_data, ii));
204
+ }
205
+ }
206
+ //fputs (RSTRING_PTR(rb_obj_as_string(INT2NUM(RARRAY_LEN(tmp_data)))),stderr);
207
+ rb_iv_set(tmp, "@data", tmp_data);
208
+
209
+ // @todo, throw an exception if no trailing slash... or add one
210
+ statsrb_write(tmp, rb_str_plus(logdir, rb_ary_entry(ns_list, i)), mode);
211
+ }
212
+
213
+ return self;
214
+ }
215
+
216
+ /**
217
+ * Parses the query string parameters.
218
+ *
219
+ * @param char * qs
220
+ * The location of the query string.
221
+ *
222
+ * @return VALUE
223
+ * The ruby hash containing the query string keys and values.
224
+ */
225
+ static VALUE statsrb_parse_qs(char *qs) {
226
+ char *qsk, *qsv;
227
+ VALUE query_string_tmp = rb_ary_new();
228
+ VALUE query_string = rb_hash_new();
229
+ qsk = strtok(qs, "&\0");
230
+ while (qsk != NULL) {
231
+ rb_ary_push(query_string_tmp, rb_str_new2(qsk));
232
+ qsk = strtok(NULL, "&\0");
233
+ }
234
+ int qslen = RARRAY_LEN(query_string_tmp);
235
+ int qsi;
236
+ for (qsi = 0; qsi < qslen; qsi++) {
237
+ qsk = strtok(RSTRING_PTR(rb_ary_entry(query_string_tmp, qsi)), "=\0");
238
+ qsv = strtok(NULL, "\0");
239
+ if (qsv != NULL) {
240
+ rb_hash_aset(query_string, rb_str_new2(qsk), rb_str_new2(qsv));
241
+ }
242
+ else if(qsk != NULL && qsv != NULL) {
243
+ rb_hash_aset(query_string, rb_str_new2(qsk), rb_str_new2(""));
244
+ }
245
+ }
246
+
247
+ return query_string;
248
+ }
249
+
250
+ /**
251
+ * A method that is compatible with the rack api.
252
+ */
253
+ static VALUE statsrb_rack_call(VALUE self, VALUE env) {
254
+ VALUE response = rb_ary_new();
255
+ VALUE headers = rb_hash_new();
256
+ VALUE body = rb_ary_new();
257
+ VALUE statsrb_data = rb_iv_get(self, "@data");
258
+ VALUE statsrb_hash = rb_hash_new();
259
+
260
+ // @data hash key symbols.
261
+ VALUE statsrb_key_ts = rb_iv_get(self, "@key_ts");
262
+ VALUE statsrb_key_ns = rb_iv_get(self, "@key_ns");
263
+ VALUE statsrb_key_v = rb_iv_get(self, "@key_v");
264
+
265
+ char *path = RSTRING_PTR(rb_hash_aref(env, rb_str_new2("PATH_INFO")));
266
+
267
+ rb_hash_aset(headers, rb_str_new2("Content-Type"), rb_str_new2("text/json"));
268
+
269
+ // Parse the query string
270
+ char *qs = RSTRING_PTR(rb_hash_aref(env, rb_str_new2("QUERY_STRING")));
271
+ VALUE query_string = statsrb_parse_qs(qs);
272
+
273
+ //const char *method = RSTRING_PTR(rb_hash_aref(env, rb_str_new2("REQUEST_METHOD")));
274
+ // @TODO consider moving the request method to the proper REQUEST_METHOD
275
+ const char *method_get = "GET";
276
+ const char *method_put = "PUT";
277
+ // Remove the leading /
278
+ path++;
279
+ const char *method = strtok(path, "/\0");
280
+ if (method && strcmp(method, method_put) == 0) {
281
+ long int statsrb_ts, statsrb_v;
282
+
283
+ // Get the timestamp, default to now.
284
+ VALUE statsrb_ts_qs = rb_hash_aref(query_string, rb_str_new("time", 4));
285
+ if (statsrb_ts_qs != Qnil) {
286
+ statsrb_ts = atoi(RSTRING_PTR(statsrb_ts_qs ));
287
+ }
288
+ else {
289
+ statsrb_ts = (long int)time(NULL);
290
+ }
291
+
292
+ // Get the namespace.
293
+ VALUE statsrb_ns = rb_hash_aref(query_string, rb_str_new("name", 4));
294
+ if (statsrb_ns == Qnil) {
295
+ statsrb_ns = NULL;
296
+ }
297
+
298
+ if (statsrb_ns) {
299
+ // Get the value.
300
+ statsrb_v= 0;
301
+ VALUE statsrb_v_qs = rb_hash_aref(query_string, rb_str_new("value", 5));
302
+ if (statsrb_v_qs != Qnil) {
303
+ statsrb_v = atoi(RSTRING_PTR(statsrb_v_qs));
304
+ }
305
+
306
+ rb_hash_aset(statsrb_hash, statsrb_key_ts, INT2NUM(statsrb_ts));
307
+ rb_hash_aset(statsrb_hash, statsrb_key_ns, statsrb_ns);
308
+ rb_hash_aset(statsrb_hash, statsrb_key_v, INT2NUM(statsrb_v));
309
+ rb_ary_push(statsrb_data, statsrb_hash);
310
+
311
+ int data_length = RARRAY_LEN(statsrb_data);
312
+ rb_ary_push(body, rb_obj_as_string(INT2NUM(RARRAY_LEN(statsrb_data))));
313
+ if (data_length > 9) {
314
+ statsrb_sort(self);
315
+ statsrb_split_write(self, rb_iv_get(self, "@split_file_dir"), rb_str_new2("a+"));
316
+ rb_ary_resize(statsrb_data, 0);
317
+ }
318
+
319
+ rb_ary_push(body, statsrb_ns);
320
+ }
321
+ }
322
+ else if (method && strcmp(method, method_get) == 0) {
323
+ const char * statsrb_str_ns = strtok(NULL, "/\0");
324
+ if (statsrb_str_ns == NULL) {
325
+ statsrb_str_ns = "data";
326
+ }
327
+
328
+ VALUE jsoncallback = rb_hash_aref(query_string, rb_str_new("jsoncallback", 12));
329
+ if (jsoncallback != Qnil) {
330
+ rb_ary_push(body, rb_str_plus(jsoncallback, rb_str_new("(", 1)));
331
+ }
332
+ char json_start[256];
333
+ sprintf(json_start, "{\"%s\":[", statsrb_str_ns);
334
+ rb_ary_push(body, rb_str_new2(json_start));
335
+
336
+ // If they didn't specify a namespace, bail out immediately.
337
+ if (statsrb_str_ns) {
338
+ VALUE statsrb_ns = rb_str_new2(statsrb_str_ns);
339
+ long int query_limit, query_start, query_end;
340
+
341
+ // Get the query limit.
342
+ query_limit = 100;
343
+ VALUE query_limit_qs = rb_hash_aref(query_string, rb_str_new("limit", 5));
344
+ if (query_limit_qs != Qnil) {
345
+ query_limit = atoi(RSTRING_PTR(query_limit_qs));
346
+ }
347
+
348
+ // Get the query start.
349
+ query_start = 0;
350
+ VALUE query_start_qs = rb_hash_aref(query_string, rb_str_new("start", 5));
351
+ if (query_start_qs != Qnil) {
352
+ query_start = atoi(RSTRING_PTR(query_start_qs));
353
+ }
354
+
355
+ // Get the query end.
356
+ query_end = 0;
357
+ VALUE query_end_qs = rb_hash_aref(query_string, rb_str_new("end", 3));
358
+ if (query_end_qs != Qnil) {
359
+ query_end = atoi(RSTRING_PTR(query_end_qs));
360
+ }
361
+
362
+ // Get the past N seconds of data.
363
+ // @TODO the query method fails if we query for data newer than the last entry.
364
+ VALUE query_recent = rb_hash_aref(query_string, rb_str_new("recent", 6));
365
+ if (query_recent != Qnil) {
366
+ query_end = (long int)time(NULL);
367
+ long int history = atoi(RSTRING_PTR(query_recent));
368
+ query_start = query_end - history;
369
+ }
370
+
371
+ // Create a new Statsrb object to query from.
372
+ // @todo we probably need to assign a new array to @data to avoid messing up the pointers.
373
+ VALUE tmp = rb_obj_dup(self);
374
+ VALUE tmp_data = rb_ary_new();
375
+ rb_iv_set(tmp, "@data", tmp_data);
376
+ statsrb_query(tmp, rb_str_plus(rb_iv_get(self, "@split_file_dir"), statsrb_ns), statsrb_ns, INT2NUM(query_limit), INT2NUM(query_start), INT2NUM(query_end));
377
+ statsrb_sort(tmp);
378
+
379
+ int i, data_length = RARRAY_LEN(tmp_data);
380
+
381
+ for (i = 0; i < data_length; i++) {
382
+ rb_ary_push(body, rb_str_new("[", 1));
383
+ rb_ary_push(body, rb_obj_as_string(rb_hash_aref(rb_ary_entry(tmp_data, i), statsrb_key_ts )));
384
+ rb_ary_push(body, rb_str_new(",", 1));
385
+ rb_ary_push(body, rb_obj_as_string(rb_hash_aref(rb_ary_entry(tmp_data, i), statsrb_key_v )));
386
+ rb_ary_push(body, rb_str_new("]", 1));
387
+ if (i < data_length - 1) {
388
+ rb_ary_push(body, rb_str_new(",", 1));
389
+ }
390
+ rb_ary_push(body, rb_str_new("\n", 1));
391
+ }
392
+ rb_ary_resize(tmp_data, 0);
393
+ }
394
+ rb_ary_push(body, rb_str_new("]}", 2));
395
+ if (jsoncallback != Qnil) {
396
+ rb_ary_push(body, rb_str_new(")", 1));
397
+ }
398
+ }
399
+ else {
400
+ rb_ary_push(response, INT2NUM(404));
401
+ rb_ary_push(response, headers);
402
+ rb_ary_push(response, body);
403
+ return response;
404
+ }
405
+
406
+ rb_ary_push(response, INT2NUM(200));
407
+ rb_ary_push(response, headers);
408
+ rb_ary_push(response, body);
409
+
410
+ return response;
411
+ }
412
+
413
+ /**
414
+ * Class constructor, sets up an instance variable.
415
+ */
416
+ static VALUE statsrb_constructor(VALUE self) {
417
+ VALUE statsrb_data = rb_ary_new();
418
+ rb_iv_set(self, "@data", statsrb_data);
419
+ VALUE statsrb_split_file_dir = rb_str_new("/tmp", 4);
420
+ rb_iv_set(self, "@split_file_dir", statsrb_split_file_dir);
421
+
422
+ // Internal symbols for :ts, :ns and :v.
423
+ VALUE statsrb_key_ts = rb_str_intern(rb_str_new2("ts"));
424
+ rb_iv_set(self, "@key_ts", statsrb_key_ts);
425
+ VALUE statsrb_key_ns = rb_str_intern(rb_str_new2("ns"));
426
+ rb_iv_set(self, "@key_ns", statsrb_key_ns);
427
+ VALUE statsrb_key_v = rb_str_intern(rb_str_new2("v"));
428
+ rb_iv_set(self, "@key_v", statsrb_key_v);
429
+
430
+ return self;
431
+ }
432
+
433
+ /**
434
+ * Init the Statsrb class.
435
+ */
436
+ void Init_statsrb(void) {
437
+ VALUE klass = rb_define_class("Statsrb", rb_cObject);
438
+
439
+ // Instance methods and properties.
440
+ rb_define_method(klass, "initialize", statsrb_constructor, 0);
441
+ rb_define_method(klass, "query", statsrb_query, 5);
442
+ rb_define_method(klass, "sort", statsrb_sort, 0);
443
+ rb_define_method(klass, "write", statsrb_write, 2);
444
+ rb_define_method(klass, "split_write", statsrb_split_write, 2);
445
+ rb_define_method(klass, "call", statsrb_rack_call, 1);
446
+ // Define :attr_accessor (read/write instance var)
447
+ // Note that this must correspond with a call to rb_iv_self() and it's string name must be @data.
448
+ rb_define_attr(klass, "data", 1, 1);
449
+ rb_define_attr(klass, "split_file_dir", 1, 1);
450
+ }
data/lib/statsrb.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'statsrb/statsrb'
2
+
3
+ # @author Kevin Hankens
4
+ class Statsrb
5
+ # [!{:ts => Time.now.to_i, :ns => "test", :v => 33}]
6
+ attr_accessor :data
7
+
8
+ # Writes the @data in memory to a specified file.
9
+ # @param filepath [String]
10
+ # @param filemode [String]
11
+ # @return Statsrb
12
+ def write filepath, filemode
13
+ end
14
+
15
+ # Splits namespaces in @data in memory to a separate files.
16
+ # @param filepath [String]
17
+ # @param filemode [String]
18
+ # @return Statsrb
19
+ def split_write filepath, filemode
20
+ end
21
+
22
+ # Locates data from a specified file and loads into @data.
23
+ # @param filepath [String]
24
+ # @param namespace [String]
25
+ # @param limit [Number]
26
+ # @param start_time [Number]
27
+ # @param end_time [Number]
28
+ # @return Statsrb
29
+ def query filepath, namespace, limit, start_time, end_time
30
+ end
31
+
32
+ # Returns a rack-compatable response.
33
+ # @param env [Hash]
34
+ def sort env
35
+ end
36
+
37
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statsrb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kevin Hankens
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-08 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A ruby stats repository.
15
+ email: email@kevinhankens.com
16
+ executables: []
17
+ extensions:
18
+ - ext/statsrb/extconf.rb
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/statsrb.rb
22
+ - ext/statsrb/statsrb.c
23
+ - ext/statsrb/extconf.rb
24
+ homepage: https://github.com/kevinhankens/statsrb
25
+ licenses: []
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ! '>='
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 1.8.25
45
+ signing_key:
46
+ specification_version: 3
47
+ summary: Statsrb
48
+ test_files: []
49
+ has_rdoc: