ailurus 2.0.0
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/.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:
|