ailurus 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.md +21 -0
- data/README.md +91 -0
- data/Rakefile +6 -0
- data/ailurus.gemspec +26 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/ailurus/client.rb +129 -0
- data/lib/ailurus/dataset/create.rb +52 -0
- data/lib/ailurus/dataset/metadata.rb +27 -0
- data/lib/ailurus/dataset/search.rb +99 -0
- data/lib/ailurus/dataset/update.rb +29 -0
- data/lib/ailurus/dataset.rb +20 -0
- data/lib/ailurus/utils.rb +19 -0
- data/lib/ailurus/version.rb +3 -0
- data/lib/ailurus.rb +2 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3f52e6a0432ea15f4e387c270afdc29775881f68
|
4
|
+
data.tar.gz: dd0bfa80ca19db9b79913815ac7017c0ed87581f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab84556e5c2b7a6257b0f2222a46893f784c4971416bd3cb51dbc29bc90c182e032a2a071c1d29088801ba5285cd2e7739b8ac87ca71dcb3d3a0d966bfee0e2b
|
7
|
+
data.tar.gz: 3c359df7c707387c26d92dc612f93114d7abb1b83dcc2356a8f5c3083a0f63f594d25a544011fa77ccae107481b64e657c7d0721e3264fd1eb648572ab4c184c
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2015 The Associated Press and contributors
|
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,91 @@
|
|
1
|
+
# Ailurus #
|
2
|
+
|
3
|
+
This is a client gem to help people work programmatically with
|
4
|
+
[PANDA](http://pandaproject.net/) instances.
|
5
|
+
|
6
|
+
## Usage ##
|
7
|
+
|
8
|
+
>> require "ailurus"
|
9
|
+
>> client = Ailurus::Client.new
|
10
|
+
>> dataset = client.dataset("DATASET_SLUG")
|
11
|
+
|
12
|
+
>> metadata = dataset.metadata
|
13
|
+
>> metadata.slug
|
14
|
+
=> "DATASET_SLUG"
|
15
|
+
|
16
|
+
>> results = dataset.search("search query")
|
17
|
+
|
18
|
+
>> dataset = client.dataset("NEW_DATASET_SLUG")
|
19
|
+
>> dataset.create([{:name => "letter", :type => "unicode"}, {:name => "number", :type => "int"}])
|
20
|
+
>> dataset.update([{"data" => ["A", "1"]}, {"data" => ["A", "2"]}])
|
21
|
+
>> dataset.search("A")
|
22
|
+
=> [["A", "1"], ["A", "2"]]
|
23
|
+
>> dataset.search("A", :max_results => 1)
|
24
|
+
=> [["A", "1"]]
|
25
|
+
|
26
|
+
For datasets with indexed fields, you can perform additional searches and sorts
|
27
|
+
(better syntax TK):
|
28
|
+
|
29
|
+
>> dataset = client.dataset("SLUG")
|
30
|
+
>> dataset.create([{:name => "name", :index => true}])
|
31
|
+
>> dataset.update([{"data" => ["alfa"]}, {"data" => ["bravo"]}, {"data" => ["charlie"]}])
|
32
|
+
>> indexed_column_name = dataset.get_indexed_name("name")
|
33
|
+
=> "column_unicode_name"
|
34
|
+
>> dataset.search("column_unicode_name:bravo")
|
35
|
+
=> [["bravo"]]
|
36
|
+
>> dataset.search("*", :options => {"sort" => "column_unicode_name desc"})
|
37
|
+
=> [["charlie"], ["bravo"], ["alfa"]]
|
38
|
+
|
39
|
+
If you want to make an API request that hasn't been implemented yet in the
|
40
|
+
client, there's a potentially useful helper function you're welcome to use:
|
41
|
+
|
42
|
+
* [Request a row by external ID](http://panda.readthedocs.org/en/1.1.1/api.html#id27)
|
43
|
+
|
44
|
+
>> client.make_request("/api/1.0/dataset/counties/data/29019/")
|
45
|
+
|
46
|
+
* [Update a row by external ID](http://panda.readthedocs.org/en/1.1.1/api.html#create-and-update)
|
47
|
+
|
48
|
+
>> client.make_request("/api/1.0/dataset/counties/data/29019/", :method => :put, :body => {"data" => ["Boone County", "Missouri"]})
|
49
|
+
|
50
|
+
* [Global search](http://panda.readthedocs.org/en/1.1.1/api.html#global-search)
|
51
|
+
|
52
|
+
>> client.make_request("/api/1.0/data/", :query => {"q" => "pie"})
|
53
|
+
|
54
|
+
`Client#make_request` will handle adding your PANDA server's domain and
|
55
|
+
[required authentication options](http://panda.readthedocs.org/en/1.1.1/api.html#api-documentation),
|
56
|
+
so you don't have to repeat any of that stuff.
|
57
|
+
|
58
|
+
Also, it returns an
|
59
|
+
[OpenStruct](http://ruby-doc.org/stdlib-2.2.1/libdoc/ostruct/rdoc/OpenStruct.html),
|
60
|
+
so you don't have to include all those extra brackets and quotes:
|
61
|
+
|
62
|
+
>> res = client.make_request("/api/1.0/dataset/counties/data/")
|
63
|
+
>> res.name
|
64
|
+
=> "U.S. Counties"
|
65
|
+
>> res.slug
|
66
|
+
=> "counties"
|
67
|
+
|
68
|
+
## Configuration ##
|
69
|
+
|
70
|
+
To interact with a PANDA server, you'll need its domain (hostname), a user's
|
71
|
+
[API key](http://panda.readthedocs.org/en/1.1.1/api_keys.html) and that user's
|
72
|
+
email address.
|
73
|
+
|
74
|
+
You'll then need to get those to your `Ailurus::Client` instance somehow when
|
75
|
+
you initialize it.
|
76
|
+
|
77
|
+
You can pass them explicitly to the constructor:
|
78
|
+
|
79
|
+
client = Ailurus::Client.new(
|
80
|
+
:api_key => "api_key_goes_here",
|
81
|
+
:domain => "panda.example.com",
|
82
|
+
:email => "somebody@example.com")
|
83
|
+
|
84
|
+
If any of those options is omitted, Ailurus will look for it in the environment
|
85
|
+
variable `PANDA_API_KEY`, `PANDA_DOMAIN` or `PANDA_EMAIL`, as appropriate.
|
86
|
+
|
87
|
+
## Name ##
|
88
|
+
|
89
|
+
Ruby client for PANDA => `ruby-panda` =>
|
90
|
+
[red panda](http://en.wikipedia.org/wiki/Red_panda) =>
|
91
|
+
_Ailurus fulgens_ (scientific name) => Ailurus
|
data/Rakefile
ADDED
data/ailurus.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "ailurus/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ailurus"
|
8
|
+
spec.version = Ailurus::VERSION
|
9
|
+
spec.authors = ["Justin Myers"]
|
10
|
+
spec.email = ["jmyers@ap.org"]
|
11
|
+
|
12
|
+
spec.summary = %q{Ruby client gem for PANDA servers}
|
13
|
+
spec.description = %q{Ruby client gem for newsroom data libraries running PANDA}
|
14
|
+
spec.homepage = "https://github.com/associatedpress/ailurus"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "climate_control"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
spec.add_development_dependency "webmock"
|
26
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ailurus"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require "json"
|
2
|
+
require "ostruct"
|
3
|
+
|
4
|
+
require "ailurus/dataset"
|
5
|
+
require "ailurus/utils"
|
6
|
+
|
7
|
+
# We can't deserialize JSON properly into an OpenStruct under Ruby 1.9.3
|
8
|
+
# because OpenStruct instances are missing a required method. Let's implement
|
9
|
+
# that if it doesn't already exist.
|
10
|
+
#
|
11
|
+
# Method source copied from 2.2.2: http://bit.ly/1FTM95x
|
12
|
+
if not OpenStruct.method_defined?("[]=".to_s)
|
13
|
+
class OpenStruct
|
14
|
+
#
|
15
|
+
# Sets the value of a member.
|
16
|
+
#
|
17
|
+
# person = OpenStruct.new('name' => 'John Smith', 'age' => 70)
|
18
|
+
# person[:age] = 42 # => equivalent to ostruct.age = 42
|
19
|
+
# person.age # => 42
|
20
|
+
#
|
21
|
+
def []=(name, value)
|
22
|
+
modifiable[new_ostruct_member(name)] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Ailurus
|
28
|
+
# Public: Initialize a client object through which to interact with a PANDA
|
29
|
+
# server.
|
30
|
+
#
|
31
|
+
# config - A Hash of configuration options, including, at a minimum:
|
32
|
+
#
|
33
|
+
# :api_key - An API key for a user on the PANDA server.
|
34
|
+
# :domain - The hostname of the PANDA server.
|
35
|
+
# :email - The email address of the PANDA user.
|
36
|
+
class Client
|
37
|
+
attr_accessor :api_key, :domain, :email
|
38
|
+
|
39
|
+
def initialize(config = {})
|
40
|
+
config.each do |key, value|
|
41
|
+
instance_variable_set("@#{key}", value)
|
42
|
+
end
|
43
|
+
|
44
|
+
[
|
45
|
+
{
|
46
|
+
:description => "API key",
|
47
|
+
:env_var => "PANDA_API_KEY",
|
48
|
+
:instance_var => :@api_key
|
49
|
+
},
|
50
|
+
{
|
51
|
+
:description => "email address",
|
52
|
+
:env_var => "PANDA_EMAIL",
|
53
|
+
:instance_var => :@email
|
54
|
+
},
|
55
|
+
{
|
56
|
+
:description => "PANDA server domain",
|
57
|
+
:env_var => "PANDA_DOMAIN",
|
58
|
+
:instance_var => :@domain
|
59
|
+
},
|
60
|
+
].each do |item|
|
61
|
+
if not self.instance_variable_defined?(item[:instance_var])
|
62
|
+
if not ENV.has_key?(item[:env_var])
|
63
|
+
raise ArgumentError, (
|
64
|
+
"No #{item[:description]} specified in arguments or " +
|
65
|
+
"#{item[:env_var]} environment variable")
|
66
|
+
end
|
67
|
+
self.instance_variable_set(item[:instance_var], ENV[item[:env_var]])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Return the parsed JSON from a given API endpoint after adding
|
73
|
+
# the appropriate domain and authentication parameters.
|
74
|
+
#
|
75
|
+
# endpoint - The path component of the URL to the desired API endpoint
|
76
|
+
# (e.g., /api/1.0/dataset/).
|
77
|
+
# options - A Hash of additional options for the request:
|
78
|
+
# :query - A Hash of query-string parameters to add to the
|
79
|
+
# request (default: none).
|
80
|
+
# :method - A Symbol specifying the HTTP method for the request
|
81
|
+
# (default: :get).
|
82
|
+
# :body - An object to be converted to JSON and used as the
|
83
|
+
# request body (default: empty).
|
84
|
+
#
|
85
|
+
# Returns the parsed JSON response, regardless of type.
|
86
|
+
def make_request(endpoint, options = {})
|
87
|
+
# Handle default option values.
|
88
|
+
query = options.fetch(:query, {})
|
89
|
+
method = options.fetch(:method, :get)
|
90
|
+
body = options.fetch(:body, nil)
|
91
|
+
|
92
|
+
req_url = URI.join(Ailurus::Utils::get_absolute_uri(@domain), endpoint)
|
93
|
+
auth_params = {
|
94
|
+
:format => "json",
|
95
|
+
:email => @email,
|
96
|
+
:api_key => @api_key
|
97
|
+
}
|
98
|
+
req_url.query = URI.encode_www_form(auth_params.merge(query))
|
99
|
+
|
100
|
+
req_class = Net::HTTP.const_get(method.to_s.capitalize)
|
101
|
+
req = req_class.new(req_url.request_uri)
|
102
|
+
|
103
|
+
if not body.nil?
|
104
|
+
req.body = JSON.generate(body)
|
105
|
+
req.content_type = "application/json"
|
106
|
+
end
|
107
|
+
|
108
|
+
res = Net::HTTP.start(req_url.hostname, req_url.port) do |http|
|
109
|
+
http.request(req)
|
110
|
+
end
|
111
|
+
|
112
|
+
if res.body && res.body.length >= 2
|
113
|
+
JSON.parse(res.body, :object_class => OpenStruct)
|
114
|
+
else
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Public: Return a Dataset instance with the given slug.
|
120
|
+
#
|
121
|
+
# slug - The slug to a PANDA Dataset, as described at
|
122
|
+
# http://panda.readthedocs.org/en/1.1.1/api.html#datasets
|
123
|
+
#
|
124
|
+
# Returns an Ailurus::Dataset.
|
125
|
+
def dataset(slug)
|
126
|
+
Ailurus::Dataset.new(self, slug)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Ailurus
|
2
|
+
class Dataset
|
3
|
+
# Public: Create this Dataset on the server.
|
4
|
+
#
|
5
|
+
# This instance's @slug will be used as its `name`, too, as that's defined
|
6
|
+
# in the API.
|
7
|
+
#
|
8
|
+
# columns - An Array of Hashes, one per column, about columns expected to
|
9
|
+
# be in the Dataset. Each Hash SHOULD contain at least a :name,
|
10
|
+
# but it also MAY contain a :type (e.g., "int", "unicode",
|
11
|
+
# "bool"--default is "unicode") and/or :index (true or false,
|
12
|
+
# depending on whether you want the column to be indexed--default
|
13
|
+
# is false) (default: none).
|
14
|
+
#
|
15
|
+
# additional_params - A Hash of other properties to set on the Dataset,
|
16
|
+
# such as description and title (default: none).
|
17
|
+
#
|
18
|
+
# Returns a metadata object, such as the one returned by Dataset#metadata.
|
19
|
+
def create(columns = [], additional_params = {})
|
20
|
+
# Start with the bare minimum.
|
21
|
+
payload = {
|
22
|
+
"name" => @slug,
|
23
|
+
"slug" => @slug
|
24
|
+
}
|
25
|
+
|
26
|
+
# Add the columns. This requires the addition of up to three separate
|
27
|
+
# parameters, each comma-delimited and in a consistent order.
|
28
|
+
column_info = {}
|
29
|
+
if not columns.empty?
|
30
|
+
column_info["columns"] = columns.each_with_index.map do |column, index|
|
31
|
+
column.fetch(:name, "column_#{index}")
|
32
|
+
end.join(",")
|
33
|
+
column_info["column_types"] = columns.map do |column|
|
34
|
+
column.fetch(:type, "unicode")
|
35
|
+
end.join(",")
|
36
|
+
column_info["typed_columns"] = columns.map do |column|
|
37
|
+
# FIXME: Probably should check whether non-false values _actually_
|
38
|
+
# are true.
|
39
|
+
column.fetch(:index, false).to_s
|
40
|
+
end.join(",")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add other properties as specified.
|
44
|
+
payload.merge!(additional_params)
|
45
|
+
|
46
|
+
# Let's do this thing!
|
47
|
+
@client.make_request(
|
48
|
+
"/api/1.0/dataset/", :method => :post,
|
49
|
+
:query => column_info, :body => payload)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ailurus
|
2
|
+
class Dataset
|
3
|
+
# Public: Retrieve metadata about this Dataset.
|
4
|
+
#
|
5
|
+
# TODO: Figure out a good way to cache this so we don't keep hitting it.
|
6
|
+
#
|
7
|
+
# Returns a Hash.
|
8
|
+
def metadata
|
9
|
+
endpoint = "/api/1.0/dataset/#{@slug}/"
|
10
|
+
@client.make_request(endpoint)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: Get the indexed name for a field so you can perform more detailed
|
14
|
+
# searches if desired.
|
15
|
+
#
|
16
|
+
# column_name - A String matching the name of a column in the Dataset.
|
17
|
+
#
|
18
|
+
# Returns a String or nil, depending on whether the field is indexed.
|
19
|
+
def get_indexed_name(field_name)
|
20
|
+
column_schema = self.metadata.column_schema
|
21
|
+
indexed_names_by_column_name = Hash[column_schema.map do |schema_entry|
|
22
|
+
[schema_entry.name, schema_entry.indexed_name]
|
23
|
+
end]
|
24
|
+
indexed_names_by_column_name[field_name]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Ailurus
|
2
|
+
class Dataset
|
3
|
+
# Internal: Retrieve a set of rows from the Dataset, specified by offset
|
4
|
+
# and length.
|
5
|
+
#
|
6
|
+
# query - A query string to use when searching the data.
|
7
|
+
# offset - The number of rows to exclude from the beginning of the results
|
8
|
+
# before returning what follows; for example, to get the last
|
9
|
+
# third of a 30-row set, you would need an offset of 20.
|
10
|
+
# limit - The maximum number of rows to return, after honoring the
|
11
|
+
# offset; for example, to get the last third of a 30-row set, you
|
12
|
+
# would need a limit of 10.
|
13
|
+
#
|
14
|
+
# Returns an Array of Arrays.
|
15
|
+
def data_rows(query = nil, offset = 0, limit = 100, additional_params = {})
|
16
|
+
endpoint = "/api/1.0/dataset/#{slug}/data/"
|
17
|
+
params = {
|
18
|
+
"offset" => offset,
|
19
|
+
"limit" => limit
|
20
|
+
}
|
21
|
+
if query.nil?
|
22
|
+
raise NotImplementedError, (
|
23
|
+
"API returns unexpected results without a query present, so query is
|
24
|
+
required for now.")
|
25
|
+
else
|
26
|
+
params["q"] = query
|
27
|
+
end
|
28
|
+
|
29
|
+
params.merge!(additional_params)
|
30
|
+
|
31
|
+
res = @client.make_request(endpoint, :query => params)
|
32
|
+
if res.objects.empty? && res.meta.next.nil?
|
33
|
+
raise RangeError, "No data available for offset #{offset}"
|
34
|
+
end
|
35
|
+
|
36
|
+
res.objects.map { |row| row.data }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Internal: Retrieve a set of rows from the Dataset, specified by page
|
40
|
+
# number and page length.
|
41
|
+
#
|
42
|
+
# query - A query string to use when searching the data.
|
43
|
+
# page_num - The 0-indexed page number of data to retrieve.
|
44
|
+
# rows_per_page - The number of rows to include on each page.
|
45
|
+
#
|
46
|
+
# Returns an Array of Arrays.
|
47
|
+
def data_page(
|
48
|
+
query = nil, page_num = 0, rows_per_page = 100, additional_params = {})
|
49
|
+
self.data_rows(
|
50
|
+
query = query,
|
51
|
+
offset = page_num * rows_per_page,
|
52
|
+
limit = rows_per_page,
|
53
|
+
additional_params = additional_params)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Search the Dataset with a given query.
|
57
|
+
#
|
58
|
+
# Queries currently are required due to some observed problems with the
|
59
|
+
# PANDA API. See Dataset#data_rows.
|
60
|
+
#
|
61
|
+
# query - A query string to use when searching the data.
|
62
|
+
#
|
63
|
+
# Returns an Array of Arrays.
|
64
|
+
def search(query = nil, options = {})
|
65
|
+
# Handle optional arguments.
|
66
|
+
max_results = options.fetch(:max_results, nil)
|
67
|
+
additional_params = options.fetch(:options, {})
|
68
|
+
|
69
|
+
rows = []
|
70
|
+
page_num = 0
|
71
|
+
|
72
|
+
while true # Warning: Infinite loop! Remember to break.
|
73
|
+
# Get the current page of results. If there aren't any results on that
|
74
|
+
# page, we're done.
|
75
|
+
begin
|
76
|
+
rows.concat(self.data_page(
|
77
|
+
query = query,
|
78
|
+
page_num = page_num,
|
79
|
+
rows_per_page = 100,
|
80
|
+
additional_params = additional_params))
|
81
|
+
rescue RangeError
|
82
|
+
break # Escape the infinite loop!
|
83
|
+
end
|
84
|
+
|
85
|
+
# If we have at least as many results as we're supposed to return,
|
86
|
+
# we're done! (Truncating as necessary, of course.)
|
87
|
+
if !max_results.nil? && rows.length >= max_results
|
88
|
+
rows.slice!(max_results, rows.length - max_results)
|
89
|
+
break # Escape the infinite loop!
|
90
|
+
end
|
91
|
+
|
92
|
+
# Move on to the next page.
|
93
|
+
page_num += 1
|
94
|
+
end
|
95
|
+
|
96
|
+
rows
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ailurus
|
2
|
+
class Dataset
|
3
|
+
# Public: Update the data in this Dataset.
|
4
|
+
#
|
5
|
+
# rows - An Array of data Hashes containing the following properties:
|
6
|
+
# "objects" - An Array of Strings in the same order as this
|
7
|
+
# Dataset's columns. If you don't know what order
|
8
|
+
# your columns are in, call Dataset#metadata and
|
9
|
+
# check the result's `column_schema` attribute.
|
10
|
+
# "external_id" - An optional String identifying this row of data.
|
11
|
+
# Providing an external ID will allow future calls
|
12
|
+
# to Dataset#update to update this row with new
|
13
|
+
# information (assuming the same ID is used for one
|
14
|
+
# of its rows) rather than create a new row
|
15
|
+
# altogether. See http://bit.ly/1zeeax1 for more
|
16
|
+
# information.
|
17
|
+
#
|
18
|
+
# Returns an OpenStruct describing the rows that were created and/or
|
19
|
+
# updated.
|
20
|
+
def update(rows = [])
|
21
|
+
@client.make_request(
|
22
|
+
"/api/1.0/dataset/#{@slug}/data/",
|
23
|
+
:method => :put,
|
24
|
+
:body => {
|
25
|
+
"objects" => rows
|
26
|
+
})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "ailurus/dataset/create"
|
2
|
+
require "ailurus/dataset/metadata"
|
3
|
+
require "ailurus/dataset/search"
|
4
|
+
require "ailurus/dataset/update"
|
5
|
+
|
6
|
+
module Ailurus
|
7
|
+
# Public: A class corresponding to a PANDA Dataset.
|
8
|
+
#
|
9
|
+
# client - An Ailurus::Client instance (see `/lib/ailurus/client.rb`).
|
10
|
+
# slug - The slug to a PANDA Dataset, as described at
|
11
|
+
# http://panda.readthedocs.org/en/1.1.1/api.html#datasets
|
12
|
+
class Dataset
|
13
|
+
attr_accessor :client, :slug
|
14
|
+
|
15
|
+
def initialize(client, slug)
|
16
|
+
@client = client
|
17
|
+
@slug = slug
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Ailurus
|
4
|
+
module Utils
|
5
|
+
# Internal: Convert a domain string into a fully qualified URI.
|
6
|
+
#
|
7
|
+
# domain_string - A string with a hostname, optional protocol/scheme and
|
8
|
+
# optional port.
|
9
|
+
#
|
10
|
+
# Returns a URI::HTTP instance.
|
11
|
+
def self.get_absolute_uri(domain_string)
|
12
|
+
uri = URI(domain_string)
|
13
|
+
if not uri.is_a?(URI::HTTP)
|
14
|
+
uri = URI("http://#{domain_string}")
|
15
|
+
end
|
16
|
+
uri
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/ailurus.rb
ADDED
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ailurus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Myers
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: climate_control
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: webmock
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Ruby client gem for newsroom data libraries running PANDA
|
84
|
+
email:
|
85
|
+
- jmyers@ap.org
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.md
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- ailurus.gemspec
|
98
|
+
- bin/console
|
99
|
+
- bin/setup
|
100
|
+
- lib/ailurus.rb
|
101
|
+
- lib/ailurus/client.rb
|
102
|
+
- lib/ailurus/dataset.rb
|
103
|
+
- lib/ailurus/dataset/create.rb
|
104
|
+
- lib/ailurus/dataset/metadata.rb
|
105
|
+
- lib/ailurus/dataset/search.rb
|
106
|
+
- lib/ailurus/dataset/update.rb
|
107
|
+
- lib/ailurus/utils.rb
|
108
|
+
- lib/ailurus/version.rb
|
109
|
+
homepage: https://github.com/associatedpress/ailurus
|
110
|
+
licenses: []
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 2.4.6
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: Ruby client gem for PANDA servers
|
132
|
+
test_files: []
|
133
|
+
has_rdoc:
|