google_fusion_tables 1.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.
@@ -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