google_fusion_tables 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,26 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ examples/credentials.yml
20
+
21
+ ## PROJECT::TEST
22
+ test/test_config.yml
23
+
24
+
25
+ ## PROJECT::SPECIFIC
26
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Kareem Hashem
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,179 @@
1
+ h1. fusion-tables
2
+
3
+ This gem lets you easily interact with Google Fusion Tables from your Ruby application.
4
+
5
+ h2. Gem Dependencies
6
+
7
+ * gdata_19 >= 1.1.2
8
+
9
+ h2. Installation
10
+
11
+ bc. gem install google_fusion_tables
12
+
13
+ h2. Rubies
14
+
15
+ Tested on:
16
+
17
+ * 1.8.7
18
+ * 1.9.2-p0
19
+
20
+ h2. To Use
21
+
22
+ bc. require 'google_fusion_tables'
23
+
24
+ or in Rails 2.3.x
25
+
26
+ bc. config.gem 'google_fusion_tables'
27
+
28
+ h2. API examples
29
+
30
+ "Twitter example":http://github.com/tokumine/fusion-tables/blob/master/examples/compare_tweets.rb
31
+ "Boris bike example":http://github.com/tokumine/fusion-tables/blob/master/examples/boris_bikes.rb
32
+ "Tests":http://github.com/tokumine/fusion-tables/tree/master/test/
33
+
34
+ Here is a brief rundown:
35
+
36
+ <pre><code># Connect to service
37
+ @ft = GData::Client::FusionTables.new
38
+ @ft.clientlogin(username, password)
39
+
40
+ # Browse existing tables
41
+ @ft.show_tables
42
+ # => [table_1, table_2]
43
+
44
+ # Getting table id suitable for using with google maps (see more below)
45
+ table_1.id #=> 42342 (the table's google id)
46
+
47
+ # Count data
48
+ table_1.count #=> 1
49
+
50
+ # Select data
51
+ table_1.select
52
+ #=> data
53
+
54
+ # Select data with conditions
55
+ table_1.select "name", "WHERE x=n"
56
+ #=> data
57
+
58
+ # Select ROWIDs
59
+ row_ids = table_1.select "ROWID"
60
+
61
+ # Drop tables
62
+ @ft.drop table_1.id # table id
63
+ @ft.drop [table_1.id, table_2.id] # arrays of table ids
64
+ @ft.drop /yacht/ # regex on table name
65
+
66
+ # Creating a table
67
+ cols = [{:name => "friend name", :type => 'string' },
68
+ {:name => "age", :type => 'number' },
69
+ {:name => "meeting time", :type => 'datetime' },
70
+ {:name => "where", :type => 'location' }]
71
+
72
+ new_table = @ft.create_table "My upcoming meetings", cols
73
+
74
+ # Inserting rows (auto chunks every 500)
75
+ data = [{"friend name" => "Eric Wimp",
76
+ "age" => 25,
77
+ "meeting time" => Time.utc(2010,"aug",10,20,15,1),
78
+ "where" => "29 Acacia Road, Nuttytown"}]
79
+ new_table.insert data
80
+
81
+ # Delete row
82
+ new_table.delete row_id
83
+ </code></pre>
84
+
85
+ Currently UPDATE query is not implemented, you could always delete all data and start over again.
86
+
87
+ But you could also do this:
88
+
89
+ <pre><code>
90
+ require 'active_support/core_ext/array/grouping'
91
+
92
+ # get the table as above..
93
+
94
+ data = []
95
+ list = table.select("*", "WHERE location contains '%20' ORDER BY permalink LIMIT 2")
96
+
97
+ list.each do |item|
98
+ data << item
99
+ id = table.select("ROWID", "WHERE permalink = '%s'" % item[:permalink])
100
+ id = id.first[:rowid]
101
+ puts "deleting %d" % id
102
+ table.delete(id)
103
+ end
104
+
105
+ #modify location, remove text that breaks longlat value..
106
+ data.map {|item| item[:location] = item[:location].gsub('%20','')}
107
+
108
+ data.in_groups_of(50, false) do |group|
109
+ puts "inserting to fusion table: %d items" % group.size
110
+ table.insert(group)
111
+ end
112
+ </code></pre>
113
+
114
+ h2. Fusion Tables secret Geospatial Sauce
115
+
116
+ *"Geolocated Tweets example":http://tables.googlelabs.com/DataSource?snapid=73106*
117
+
118
+ Fusion Tables is a labs product from Google. You can "read more here":http://tables.googlelabs.com/, but the key thing is that it gives you *access to the google tile mill for fast generation of google map layers across large datasets*
119
+
120
+ Fusion Tables supports the following geometry types:
121
+
122
+ * lat/long
123
+ * addresses (automatically geocodes them for you)
124
+ * KML (point, polyline, polygon, multipolygon)
125
+
126
+ h2. Integrate with google maps v3
127
+
128
+ Adding a fusion tables datalayer with many points/polygons to your v3 map is as simple as:
129
+
130
+ bc. layer = new google.maps.FusionTablesLayer(139529);
131
+
132
+ That's it
133
+
134
+ You can also refine the tiles by SQL, and can even do so dynamically:
135
+
136
+ <pre><code>
137
+ layer = new google.maps.FusionTablesLayer(198945, {
138
+ query: "SELECT address FROM 198945 WHERE ridership > 5000"}
139
+ );
140
+ </code></pre>
141
+
142
+ Finally, fusion tables also lets you make Heatmaps
143
+
144
+ <pre><code>
145
+ layer = new google.maps.FusionTablesLayer(136705, {
146
+ heatmap: true
147
+ });
148
+ </code></pre>
149
+
150
+ You can also export your data (filtered and geocoded) to KML. As an example, here are "all the Gasoline filling stations in the UK":http://tables.googlelabs.com/exporttable?query=select+col0%2Ccol1%2Ccol2%2Ccol3%2Ccol4%2Ccol5%2Ccol6%2Ccol12%2Ccol13%2Ccol14%2Ccol15%2Ccol16%2Ccol17%2Ccol18%2Ccol19%2Ccol20%2Ccol21+from+214045+&o=kmllink&g=col0
151
+
152
+ read "more here":http://code.google.com/apis/maps/documentation/javascript/overlays.html#FusionTables
153
+
154
+ h2. Known Issues
155
+
156
+ # The gem uses the Google gdata_19 gem which conflicts with the GData2 gem. Uninstall gdata2 to regain sanity.
157
+ # Currently you have to make a table public before you can display it on a map, unfortunately, this can only be done on the web interface. A suggested workaround is to put all your data in 1 big public table, and then query for the data you want to display based off a key/flag column
158
+
159
+ h2. Note on Patches/Pull Requests
160
+
161
+ * Fork the project.
162
+ * Make your feature addition or bug fix.
163
+ * Add tests for it. This is important so I don't break it in a
164
+ future version unintentionally.
165
+ * Commit, do not mess with rakefile, version, or history.
166
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
167
+ * Send me a pull request. Bonus points for topic branches.
168
+
169
+ h2. Acknowledgments
170
+
171
+ A huge thank you to: Tom Verbeure, Simon Tokumine
172
+
173
+ h2. Copyright
174
+
175
+ Copyright (c) 2011 Kareem Hashem. See LICENSE for details.
176
+
177
+
178
+
179
+
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "google_fusion_tables"
8
+ gem.summary = %Q{Google Fusion Tables API wrapper}
9
+ gem.description = %Q{A simple Google Fusion Tables API wrapper. Supports bulk inserts and most API functions}
10
+ gem.email = "eng.kareem.hashem@gmail.com"
11
+ gem.homepage = "http://github.com/kimohashem/fusion-tables"
12
+ gem.authors = ["Kareem Hashem"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_dependency "gdata_19", ">= 1.1.2"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/test_*.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+ task :test => :check_dependencies
43
+
44
+ task :default => :test
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "fusion_tables #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.2
@@ -0,0 +1,143 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Twitter Fusion Tables Mashup
4
+ # S.Tokumine 2010
5
+ #
6
+ # looks for tweets in the live stream around certain
7
+ # cities and posts them to fusion tables with KML attached
8
+ #
9
+ # Gem dependencies:
10
+ #
11
+ # tweetstream
12
+ # GeoRuby
13
+ # fusion_tables
14
+ #
15
+ # Output from running this for an evening
16
+ # http://tables.googlelabs.com/DataSource?snapid=72509
17
+ #
18
+ require 'rubygems'
19
+ require 'tweetstream'
20
+ require 'geo_ruby'
21
+ include GeoRuby
22
+ include SimpleFeatures
23
+ require 'fusion_tables'
24
+ require 'time'
25
+ require 'yaml'
26
+
27
+ class Object
28
+ def try(method, *args, &block)
29
+ send(method, *args, &block)
30
+ end
31
+ end
32
+
33
+ # Configure settings
34
+ config = YAML::load_file(File.join(File.dirname(__FILE__), 'credentials.yml'))
35
+ DEFAULT_SRID = 4328
36
+
37
+ # Twitter places
38
+ places = {
39
+ :san_francisco => [-122.75,36.8,-121.75,37.8],
40
+ :new_york => [-74,40,-73,41],
41
+ :tokyo => [139.3,35,140.3,36],
42
+ :london => [-0.54,51.2,0.46,52.2],
43
+ :madrid => [-4.2,40,-3.2,41],
44
+ :paris => [1.75,48.5,2.75, 49.5],
45
+ :beijing => [115.9,39,116.9,40],
46
+ :mumbai => [72.75,18.88,73.75,19.88],
47
+ }
48
+
49
+ # Configure fusion tables
50
+ ft = GData::Client::FusionTables.new
51
+ ft.clientlogin(config["google_username"], config["google_password"])
52
+ table_name = "TwitterFusion"
53
+ cols = [
54
+ {:name => 'screen_name', :type => 'string'},
55
+ {:name => 'avatar', :type => 'string'},
56
+ {:name => 'text', :type => 'string'},
57
+ {:name => 'created', :type => 'datetime'},
58
+ {:name => 'url', :type => 'string'},
59
+ {:name => 'location', :type => 'location'},
60
+ {:name => 'iso', :type => 'location'},
61
+ {:name => 'country_name', :type => 'location'},
62
+ {:name => 'city', :type => 'string'}
63
+ ]
64
+
65
+ # Create FT if it doesn't exist
66
+ tables = ft.show_tables
67
+ table = tables.select{|t| t.name == table_name}.first
68
+ table = ft.create_table(table_name, cols) if !table
69
+
70
+ # Configure Twitter stream client
71
+ data = []
72
+ tw = TweetStream::Client.new(config["twitter_username"],config["twitter_password"])
73
+
74
+ # configure friendly rate limit handling
75
+ tw.on_limit do |skip_count|
76
+ sleep 5
77
+ end
78
+
79
+ # start searching twitter stream and posting to FT
80
+ tw.filter(:locations => places.values.join(",")) do |tweet|
81
+ begin
82
+
83
+ country = "unknown"
84
+ iso = "unknown"
85
+ begin
86
+ country = tweet.try(:[],:place).try(:[], :country)
87
+ iso = tweet.try(:[],:place).try(:[], :country_code)
88
+ rescue
89
+ end
90
+
91
+ # Divine the tweets geometry
92
+ #
93
+ # overly complex due to
94
+ # * some US tweets have their lat/longs flipped (but not all...)
95
+ # * some geo tweets are made using a "place" envelope rather than exact lat/kng
96
+ if tweet[:geo]
97
+ if iso == 'US' && tweet[:geo][:coordinates][1] > 0
98
+ p = Point.from_x_y(tweet[:geo][:coordinates][0],tweet[:geo][:coordinates][1])
99
+ else
100
+ p = Point.from_x_y(tweet[:geo][:coordinates][1],tweet[:geo][:coordinates][0])
101
+ end
102
+ else
103
+ p = Polygon.from_coordinates(tweet[:place][:bounding_box][:coordinates]).envelope.center
104
+ end
105
+
106
+ # work out which city the tweet is from by testing with an extended bounding box
107
+ # BBox extention needed as twitter returns things outside our defined bboxes...
108
+ city = "unknown"
109
+ places.each do |key, value|
110
+ if !(p.x < value[0]-1 || p.x > value[2]+1 || p.y < value[1]-1 || p.y > value[3]+1)
111
+ city = key.to_s.gsub("_"," ")
112
+ break
113
+ end
114
+ end
115
+
116
+ # pack data
117
+ data << {
118
+ "screen_name" => tweet[:user][:screen_name],
119
+ "avatar" => tweet[:user][:profile_image_url],
120
+ "text" => tweet[:text],
121
+ "created" => Time.parse(tweet[:created_at]),
122
+ "url" => "http://twitter.com/#{tweet[:user][:screen_name]}/status/#{tweet[:id]}",
123
+ "location" => p.as_kml,
124
+ "iso" => iso,
125
+ "country_name" => country,
126
+ "city" => city
127
+ }
128
+ rescue => e
129
+ puts "ERROR: #{e.inspect}, #{e.backtrace}"
130
+ #let sleeping dogs lie...
131
+ end
132
+
133
+ # let us know how we're doing
134
+ puts "#{50-data.size}: #{city}, #{tweet.text}"
135
+
136
+ # Post to fusion tables
137
+ if data.size == 50
138
+ puts "sending data to fusion tables..."
139
+ ft_data = data
140
+ data = []
141
+ table.insert ft_data
142
+ end
143
+ end
@@ -0,0 +1,4 @@
1
+ twitter_username: your_twitter_username
2
+ twitter_password: your_twitter_password
3
+ google_username: your_google_username
4
+ google_password: your_google_password
@@ -0,0 +1,76 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{google_fusion_tables}
8
+ s.version = "1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Kareem Hashem"]
12
+ s.date = %q{2011-01-26}
13
+ s.description = %q{A simple Google Fusion Tables API wrapper. Supports bulk inserts and most API functions}
14
+ s.email = %q{eng.kareem.hashem@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.textile"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.textile",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "examples/compare_tweets.rb",
27
+ "examples/credentials.example.yml",
28
+ "fusion_tables.gemspec",
29
+ "lib/fusion_tables.rb",
30
+ "lib/fusion_tables/client/fusion_tables.rb",
31
+ "lib/fusion_tables/data/data.rb",
32
+ "lib/fusion_tables/data/table.rb",
33
+ "lib/fusion_tables/ext/fusion_tables.rb",
34
+ "pkg/fusion_tables-0.1.0.gem",
35
+ "pkg/fusion_tables-0.1.1.gem",
36
+ "pkg/fusion_tables-0.1.2.gem",
37
+ "pkg/fusion_tables-0.2.0.gem",
38
+ "pkg/fusion_tables-0.2.1.gem",
39
+ "pkg/fusion_tables-0.2.2.gem",
40
+ "test/README",
41
+ "test/helper.rb",
42
+ "test/test_client.rb",
43
+ "test/test_config.yml.sample",
44
+ "test/test_ext.rb",
45
+ "test/test_table.rb"
46
+ ]
47
+ s.homepage = %q{http://github.com/kimohashem/fusion-tables}
48
+ s.rdoc_options = ["--charset=UTF-8"]
49
+ s.require_paths = ["lib"]
50
+ s.rubygems_version = %q{1.3.7}
51
+ s.summary = %q{Google Fusion Tables API wrapper}
52
+ s.test_files = [
53
+ "test/helper.rb",
54
+ "test/test_client.rb",
55
+ "test/test_ext.rb",
56
+ "test/test_table.rb",
57
+ "examples/compare_tweets.rb"
58
+ ]
59
+
60
+ if s.respond_to? :specification_version then
61
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
65
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
66
+ s.add_runtime_dependency(%q<gdata_19>, [">= 1.1.2"])
67
+ else
68
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
69
+ s.add_dependency(%q<gdata_19>, [">= 1.1.2"])
70
+ end
71
+ else
72
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
73
+ s.add_dependency(%q<gdata_19>, [">= 1.1.2"])
74
+ end
75
+ end
76
+
@@ -0,0 +1,6 @@
1
+ require 'csv'
2
+ require 'gdata'
3
+ require 'fusion_tables/client/fusion_tables'
4
+ require 'fusion_tables/ext/fusion_tables'
5
+ require 'fusion_tables/data/data'
6
+ require 'fusion_tables/data/table'
@@ -0,0 +1,76 @@
1
+ # Copyright (C) 2010 Tom Verbeure, Simon Tokumine
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module GData
16
+ module Client
17
+ class FusionTables < Base
18
+
19
+ SERVICE_URL = "https://tables.googlelabs.com/api/query"
20
+ DATATYPES = %w(number string location datetime)
21
+
22
+ def initialize(options = {})
23
+ options[:clientlogin_service] ||= 'fusiontables'
24
+ options[:headers] = { 'Content-Type' => 'application/x-www-form-urlencoded' }
25
+ super(options)
26
+ end
27
+
28
+ def sql_encode(sql)
29
+ "sql=" + CGI::escape(sql)
30
+ end
31
+
32
+ def sql_get(sql)
33
+ resp = self.get(SERVICE_URL + "?" + sql_encode(sql))
34
+ end
35
+
36
+ def sql_post(sql)
37
+ resp = self.post(SERVICE_URL, sql_encode(sql))
38
+ end
39
+
40
+ def sql_put(sql)
41
+ resp = self.put(SERVICE_URL, sql_encode(sql))
42
+ end
43
+
44
+ # Overrides auth_handler= so if the authentication changes,
45
+ # the session cookie is cleared.
46
+ def auth_handler=(handler)
47
+ @session_cookie = nil
48
+ return super(handler)
49
+ end
50
+
51
+ # Overrides make_request to handle 500 redirects with a session cookie.
52
+ def make_request(method, url, body = '', retries = 10)
53
+ begin
54
+ response = super(method, url, body)
55
+ rescue GData::Client::ServerError => e
56
+ if e.response.status_code == 500 and retries > 0
57
+ sleep_time = 11 - retries
58
+ sleep sleep_time # <= Fusion tables has rate of 5 calls per second. Be nice, get longer
59
+ @session_cookie = e.response.headers['set-cookie']
60
+ return self.make_request(method, url, body, retries - 1)
61
+ else
62
+ return e.response
63
+ end
64
+ end
65
+ end
66
+
67
+ # Custom prepare_headers to include the session cookie if it exists
68
+ def prepare_headers
69
+ if @session_cookie
70
+ @headers['cookie'] = @session_cookie
71
+ end
72
+ super
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright (C) 2010 Simon Tokumine
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module GData
16
+ module Client
17
+ class FusionTables < Base
18
+ class Data
19
+ include Enumerable
20
+
21
+ attr_reader :headers, :body, :live
22
+ alias :to_h :body
23
+
24
+ # configures headers hash and sets up
25
+ def initialize options
26
+ @headers = options[:headers]
27
+ @body = options[:body]
28
+ end
29
+
30
+ # Reads in CSV
31
+ def self.parse response
32
+ body = []
33
+ headers = []
34
+
35
+ first = true
36
+ if CSV.const_defined? :Reader
37
+ CSV::Reader.parse(response.body) do |row|
38
+ if first
39
+ first = false
40
+ headers = row.map { |x|x.strip.downcase.gsub(" ","_").to_sym }
41
+ next
42
+ end
43
+ body << Hash[*headers.zip(row).flatten]
44
+ end
45
+ else
46
+ CSV.parse(response.body) do |row|
47
+ if first
48
+ first = false
49
+ headers = row.map { |x|x.strip.downcase.gsub(" ","_").to_sym }
50
+ next
51
+ end
52
+ body << Hash[*headers.zip(row).flatten]
53
+ end
54
+ end
55
+ self.new :headers => headers, :body => body
56
+ end
57
+
58
+ # Implement enumerable
59
+ def each
60
+ @body.each { |i| yield i }
61
+ end
62
+
63
+ private
64
+ # Encodes row according to type
65
+ def encode
66
+
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,146 @@
1
+ # Copyright (C) 2010 Simon Tokumine
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module GData
16
+ module Client
17
+ class FusionTables < Base
18
+ class Table
19
+ attr_reader :headers, :id, :name
20
+
21
+ # configures headers hash and sets up
22
+ #
23
+ # eg options: {:table_id => "x", :name => "y"}
24
+ #
25
+ def initialize client, options
26
+ raise ArgumentError, "need ft client" if client.class != GData::Client::FusionTables
27
+ raise ArgumentError, "need table_id and name hash" if !options.has_key?(:name) || !options.has_key?(:table_id)
28
+ @client = client
29
+ @id = options[:table_id]
30
+ @name = options[:name]
31
+ end
32
+
33
+ # Sets up data types from google
34
+ #
35
+ def describe
36
+ @client.execute "DESCRIBE #{@id}"
37
+ end
38
+
39
+ # Runs select and returns data obj
40
+ #
41
+ # Define columns and SQL conditions separatly
42
+ #
43
+ # See http://code.google.com/apis/fusiontables/docs/developers_reference.html#Select
44
+ #
45
+ # use columns=ROWID to select row ids
46
+ #
47
+ def select columns="*", conditions=nil
48
+ @client.execute "SELECT #{columns} FROM #{@id} #{conditions}"
49
+ end
50
+
51
+ # Returns a count of rows. SQL conditions optional
52
+ #
53
+ # Note: handles odd FT response: when table has 0 rows, returns empty array.
54
+ def count conditions=nil
55
+ result = select("count()", conditions)
56
+ result.empty? ? 0 : result.first[:"count()"].to_i
57
+ end
58
+
59
+
60
+ # Outputs data to an array of concatenated INSERT SQL statements
61
+ #
62
+ # format should be:
63
+ #
64
+ # [{:col_1 => data, :col2 => data}, {:col_1 => data, :col2 => data}]
65
+ #
66
+ # Fields are escaped and formatted for FT based on type
67
+ #
68
+ def insert data
69
+ data = [data] unless data.respond_to?(:to_ary)
70
+
71
+ # encode values to insert
72
+ data = encode data
73
+
74
+ # Chunk up the data and send
75
+ chunk = ""
76
+ data.each_with_index do |d,i|
77
+ chunk << "INSERT INTO #{@id} (#{ d.keys.join(",") }) VALUES (#{ d.values.join(",") });"
78
+ if (i+1) % 10 == 0 || (i+1) == data.size
79
+ begin
80
+ @client.sql_post(chunk)
81
+ chunk = ""
82
+ rescue => e
83
+ raise "INSERT to table:#{@id} failed on row #{i} with #{e}"
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+
90
+ # Runs update on row specified and return data obj
91
+ def update row_id, data
92
+ data = encode([data]).first
93
+ data = data.to_a.map{|x| x.join("=")}.join(", ")
94
+ @client.execute "UPDATE #{@id} SET #{data} WHERE ROWID = '#{row_id}'"
95
+ end
96
+
97
+ # delete row
98
+ def delete row_id
99
+ @client.execute "DELETE FROM #{@id} WHERE rowid='#{row_id}'"
100
+ end
101
+
102
+ # delete all rows
103
+ def truncate!
104
+ @client.execute "DELETE FROM #{@id}"
105
+ end
106
+
107
+ def get_headers
108
+ @headers ||= describe
109
+ end
110
+
111
+ def encode data
112
+ data.inject([]) do |ar,h|
113
+ ret = {}
114
+ h.each do |key, value|
115
+ if value.nil?
116
+ #empty string for nils
117
+ ret["'#{key.to_s}'"] = "''"
118
+ else
119
+ ret["'#{key.to_s}'"] = case get_datatype(key)
120
+ when "number" then "#{value}"
121
+ when "datetime" then "'#{value.strftime("%m-%d-%Y %H:%M:%S")}'"
122
+ else "'#{value.gsub(/\\/, '\&\&').gsub(/'/, "''")}'"
123
+ end
124
+ end
125
+ end
126
+ ar << ret
127
+ ar
128
+ end
129
+ end
130
+
131
+ #
132
+ # Returns datatype of given column name
133
+ #
134
+ def get_datatype column_name
135
+ get_headers
136
+ @headers.each do |h|
137
+ return h[:type] if h[:name].force_encoding('utf-8') == column_name.to_s
138
+ end
139
+ raise ArgumentError, "The column #{column_name} doesn't exist"
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+
@@ -0,0 +1,124 @@
1
+ # Copyright (C) 2010 Tom Verbeure, Simon Tokumine
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module GData
16
+ module Client
17
+ class FusionTables < Base
18
+
19
+ # Helper method to run FT SQL and return FT data object
20
+ def execute(sql)
21
+ http_req = sql.upcase.match(/^(DESCRIBE|SHOW|SELECT)/) ? :sql_get : :sql_post
22
+ GData::Client::FusionTables::Data.parse(self.send(http_req, sql)).body
23
+ end
24
+
25
+ # Show a list of fusion tables
26
+ def show_tables
27
+ data = self.execute "SHOW TABLES"
28
+
29
+ data.inject([]) do |x, row|
30
+ x << GData::Client::FusionTables::Table.new(self, row)
31
+ x
32
+ end
33
+ end
34
+
35
+ # Create a new table. Return the corresponding table
36
+ #
37
+ # Columns specified as [{:name => 'my_col_name', :type => 'my_type'}]
38
+ #
39
+ # Type must be one of:
40
+ #
41
+ # * number
42
+ # * string
43
+ # * location
44
+ # * datetime
45
+ #
46
+ def create_table(table_name, columns)
47
+
48
+ # Sanity check name
49
+ table_name = table_name.strip.gsub(/ /,'_')
50
+ # surrounded the table_name with '' to support the non latin languages
51
+ table_name = "'"+table_name+"'"
52
+ # ensure all column types are valid
53
+ columns.each do |col|
54
+ if !DATATYPES.include? col[:type].downcase
55
+ raise ArgumentError, "Ensure input types are: 'number', 'string', 'location' or 'datetime'"
56
+ end
57
+ end
58
+
59
+ # generate sql
60
+ fields = columns.map{ |col| "'#{col[:name]}': #{col[:type].upcase}" }.join(", ")
61
+ sql = "CREATE TABLE #{table_name} (#{fields})"
62
+
63
+ # create table
64
+ resp = self.sql_post(sql)
65
+ raise "unknown column type" if resp.body == "Unknown column type."
66
+
67
+ # construct table object and return
68
+ table_id = resp.body.split("\n")[1].chomp.to_i
69
+ table = GData::Client::FusionTables::Table.new(self, :table_id => table_id, :name => table_name)
70
+ table.get_headers
71
+ table
72
+ end
73
+
74
+ def create_view(table_id, view_name, columns, filter)
75
+ # Sanity check name
76
+ view_name = view_name.strip.gsub(/ /,'_')
77
+ # surrounded the table_name with '' to support the non latin languages
78
+ view_name = "'"+view_name+"'"
79
+
80
+
81
+ # generate sql
82
+ fields = columns.collect{|x| "'"+x+"'"}.join(", ")
83
+ sql = "CREATE VIEW #{view_name} AS (SELECT #{fields} FROM #{table_id})"
84
+ unless filter.empty?
85
+ filter = filter.map{ |col| "'#{col[:name]}' = '#{col[:value]}'" }.join(" AND ")
86
+ sql = "CREATE VIEW #{view_name} AS (SELECT #{fields} FROM #{table_id} WHERE #{filter})"
87
+ else
88
+ sql = "CREATE VIEW #{view_name} AS (SELECT #{fields} FROM #{table_id})"
89
+ end
90
+ # create view
91
+ resp = self.sql_post(sql)
92
+ raise "unknown column type" if resp.body == "Unknown column type."
93
+ end
94
+
95
+ # Drops Fusion Tables
96
+ #
97
+ # options can be:
98
+ #
99
+ # * an integer for single drop
100
+ # * array of integers for multi drop
101
+ # * a regex against table_name for flexible multi_drop
102
+ #
103
+ def drop(options)
104
+ # collect ids
105
+ ids = []
106
+ ids << options if options.class == Integer || options.class == String || Fixnum
107
+ ids = options if options.class == Array
108
+
109
+ if options.class == Regexp
110
+ tables = show_tables
111
+ ids = tables.map { |table| table.id if options =~ table.name }.compact
112
+ end
113
+
114
+ # drop tables
115
+ delete_count = 0
116
+ ids.each do |id|
117
+ resp = self.sql_post("DROP TABLE #{id}")
118
+ delete_count += 1 if resp.body.strip.downcase == 'ok'
119
+ end
120
+ delete_count
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ To run the tests, make a test_config.yml file using test/test_config.yml.sample as a base
2
+
3
+ If you get GData::Client::CaptchaError's, that usually means your credentials are not correct
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'yaml'
5
+
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ require 'fusion_tables'
9
+
10
+ class Test::Unit::TestCase
11
+
12
+ def init_config
13
+ if not defined? @config_file
14
+ begin
15
+ @config_file = YAML::load_file(File.join(File.dirname(__FILE__), 'test_config.yml'))
16
+ rescue
17
+ puts "Please configure your test_config.yml file using test_config.yml.sample as base"
18
+ end
19
+ end
20
+ @config_file
21
+ end
22
+
23
+ def username
24
+ @config_file['username']
25
+ end
26
+
27
+ def password
28
+ @config_file['password']
29
+ end
30
+
31
+ def table_name
32
+ @config_file['table_name']
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ require 'helper'
2
+
3
+ class TestClient < Test::Unit::TestCase
4
+
5
+ context "The fusion_tables client library" do
6
+ setup do
7
+ init_config
8
+ @ft = GData::Client::FusionTables.new
9
+ @ft.clientlogin(username, password)
10
+ end
11
+
12
+ should "be properly setup" do
13
+ assert_equal @ft.clientlogin_service, "fusiontables"
14
+ assert_equal @ft.headers["Content-Type"], "application/x-www-form-urlencoded"
15
+ end
16
+
17
+ should "be able to authenticate with the google services" do
18
+ assert_equal @ft.auth_handler.service, "fusiontables"
19
+ assert @ft.auth_handler.token
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ username: test
2
+ password: test
3
+ table_name: "test_table"
@@ -0,0 +1,58 @@
1
+ require 'helper'
2
+
3
+ class TestExt < Test::Unit::TestCase
4
+
5
+ context "The Fusion Tables helper functions" do
6
+ setup do
7
+ init_config
8
+ @ft = GData::Client::FusionTables.new
9
+ @ft.clientlogin(username, password)
10
+ end
11
+
12
+
13
+ should "raise ArgumentError if supply unknown types to it" do
14
+ assert_raise ArgumentError do
15
+ @ft.create_table "test table", [{:name => "test_col", :type => "billys birthday" }]
16
+ end
17
+ end
18
+
19
+ should "let you create a table if you get everything right" do
20
+ table = @ft.create_table "test_table", [{:name => "test_col", :type => "string" }]
21
+ assert_equal table.class, GData::Client::FusionTables::Table
22
+ @ft.drop(table.id)
23
+ end
24
+
25
+ should "correct your table name to a certain degree on create" do
26
+ table = @ft.create_table "test table", [{:name => "test col", :type => "string" }]
27
+ assert_equal table.name, "test_table"
28
+ @ft.drop(table.id)
29
+ end
30
+
31
+ should "return you a list of your fusion tables" do
32
+ resp = @ft.show_tables
33
+ assert_equal resp.first.class, GData::Client::FusionTables::Table if resp.first
34
+ end
35
+
36
+ should "be possible to delete a table with an id" do
37
+ table = @ft.create_table "test_table", [{:name => "test col", :type => "string" }]
38
+ assert_equal @ft.drop(table.id), 1
39
+ end
40
+
41
+ should "be possible to delete tables with an array of ids" do
42
+ table1 = @ft.create_table "test_table", [{:name => "test col", :type => "string" }]
43
+ table2 = @ft.create_table "test_table", [{:name => "test col", :type => "string" }]
44
+ assert_equal @ft.drop([table1.id, table2.id]), 2
45
+ end
46
+
47
+ should "be possible to delete multiple tables with a regex" do
48
+ table1 = @ft.create_table "test_table", [{:name => "test col", :type => "string" }]
49
+ table2 = @ft.create_table "test_table", [{:name => "test col", :type => "string" }]
50
+ assert_equal @ft.drop(/^test_/), 2
51
+ end
52
+
53
+ should "return zero if passed a silly id" do
54
+ assert_equal @ft.drop(235243875629384756), 0
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,76 @@
1
+ require 'helper'
2
+
3
+ class TestTable < Test::Unit::TestCase
4
+
5
+ context "uploading data to FT" do
6
+ setup do
7
+ init_config
8
+ @ft = GData::Client::FusionTables.new
9
+ @ft.clientlogin(username, password)
10
+ @table = @ft.create_table "test", [{:name => 'firstname', :type => 'string'},
11
+ {:name => 'phone', :type => 'number'},
12
+ {:name => 'dob', :type => 'datetime'},
13
+ {:name => 'house', :type => 'location'}]
14
+ end
15
+
16
+ should "format data and prep for upload" do
17
+ data = @table.encode [{:firstname => "\\bob's piz\za",
18
+ :phone => 12,
19
+ :dob => Time.utc(2010,"aug",10,20,15,1),
20
+ :house => "POINT(1,1)"}]
21
+
22
+ row = data.first
23
+ assert_equal row[:firstname], "'\\\\bob''s pizza'"
24
+ assert_equal row[:phone], "#{12}"
25
+ assert_equal row[:dob], "'08-10-2010'"
26
+ assert_equal row[:house], "'POINT(1,1)'"
27
+ end
28
+
29
+ should "be able to insert 1 row of data" do
30
+ data = 1.times.inject([]) { |a,i|
31
+ a << {:firstname => "\\bob's piz\za-#{i}",
32
+ :phone => 12,
33
+ :dob => Time.utc(2010,"aug",10,20,15,1),
34
+ :house => '<Point><coordinates>-74.006393,40.714172,0</coordinates></Point>'}
35
+ }
36
+
37
+ @table.insert data
38
+ end
39
+
40
+ should "be able to insert 501 rows of data" do
41
+ data = 501.times.inject([]) { |a,i|
42
+ a << {:firstname => "Person-#{i}",
43
+ :phone => 12,
44
+ :dob => Time.utc(2010,"aug",10,20,15,1),
45
+ :house => "<Point><coordinates>#{180-rand(360)},#{90-rand(180)},0</coordinates></Point>"}
46
+ }
47
+
48
+ @table.insert data
49
+ end
50
+
51
+
52
+ should "be able to count the number of rows" do
53
+ data = 2.times.inject([]) { |a,i|
54
+ a << {:firstname => "Person-#{i}",
55
+ :phone => 12,
56
+ :dob => Time.utc(2010,"aug",10,20,15,1),
57
+ :house => "<Point><coordinates>#{180-rand(360)},#{90-rand(180)},0</coordinates></Point>"}
58
+ }
59
+
60
+ @table.insert data
61
+ assert_equal @table.count, 2
62
+ end
63
+
64
+ should "be able to select the rows" do
65
+ data = 2.times.inject([]) { |a,i|
66
+ a << {:firstname => "Person-#{i}",
67
+ :phone => 12,
68
+ :dob => Time.utc(2010,"aug",10,20,15,1),
69
+ :house => "<Point><coordinates>1,1,0</coordinates></Point>"}
70
+ }
71
+
72
+ @table.insert data
73
+ assert_equal @table.select, [{:firstname=>"Person-0", :phone=>"12", :dob=>"08-10-2010", :house=>"<Point><coordinates>1,1,0</coordinates></Point>"}, {:firstname=>"Person-1", :phone=>"12", :dob=>"08-10-2010", :house=>"<Point><coordinates>1,1,0</coordinates></Point>"}]
74
+ end
75
+ end
76
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: google_fusion_tables
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kareem Hashem
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-01-26 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thoughtbot-shoulda
16
+ requirement: &19191900 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *19191900
25
+ - !ruby/object:Gem::Dependency
26
+ name: gdata_19
27
+ requirement: &19191420 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.2
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *19191420
36
+ description: A simple Google Fusion Tables API wrapper. Supports bulk inserts and
37
+ most API functions
38
+ email: eng.kareem.hashem@gmail.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.textile
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.textile
49
+ - Rakefile
50
+ - VERSION
51
+ - examples/compare_tweets.rb
52
+ - examples/credentials.example.yml
53
+ - fusion_tables.gemspec
54
+ - lib/fusion_tables.rb
55
+ - lib/fusion_tables/client/fusion_tables.rb
56
+ - lib/fusion_tables/data/data.rb
57
+ - lib/fusion_tables/data/table.rb
58
+ - lib/fusion_tables/ext/fusion_tables.rb
59
+ - pkg/fusion_tables-0.1.0.gem
60
+ - pkg/fusion_tables-0.1.1.gem
61
+ - pkg/fusion_tables-0.1.2.gem
62
+ - pkg/fusion_tables-0.2.0.gem
63
+ - pkg/fusion_tables-0.2.1.gem
64
+ - pkg/fusion_tables-0.2.2.gem
65
+ - test/README
66
+ - test/helper.rb
67
+ - test/test_client.rb
68
+ - test/test_config.yml.sample
69
+ - test/test_ext.rb
70
+ - test/test_table.rb
71
+ homepage: http://github.com/kimohashem/fusion-tables
72
+ licenses: []
73
+ post_install_message:
74
+ rdoc_options:
75
+ - --charset=UTF-8
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 1.8.15
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Google Fusion Tables API wrapper
96
+ test_files:
97
+ - test/helper.rb
98
+ - test/test_client.rb
99
+ - test/test_ext.rb
100
+ - test/test_table.rb
101
+ - examples/compare_tweets.rb