google-maps-scraper-sdk 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a6db571d98b81c21e14ffeaec282c78869ed47d73c96146dce26bfb57c9a283b
4
+ data.tar.gz: 072c115cbcd6fe6666b6d04c22c03be038145612d53773f2058bad776e6d226a
5
+ SHA512:
6
+ metadata.gz: 208386cbbf47b5dc9afd319b5ddcfa9304772def7b4a3d5169bb9029fee378e137c827b4e0263722a942f0e480196f5e88702955107f570a910cfcc1bec6ba19
7
+ data.tar.gz: c9516bfef13d8bf73e83571989f9a35f4995e79716c2bc0c516aff347769df77ffb3cfeb5e7178dc521d53722fc462959113074df3199287147d28d18ad6201e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GMaps Lead Finder
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # Google Maps Scraper
2
+
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/google-maps-lead-scraper/google-maps-scraper/main/google-maps-scraper.png" alt="Google Maps Lead Scraper" width="100%" />
5
+ </p>
6
+
7
+ **Google Maps Extractor · Google Maps Lead Scraper · Google Maps Lead Extractor**
8
+
9
+ Ruby SDK to scrape Google Maps places and export leads (name, phone, website, emails, and more) through a hosted Agent HTTP API. Powered by [GMaps Lead Finder](https://gmapsleadfinder.com). This is **not** a local browser crawler — jobs run in the cloud scrape-and-enrich pipeline.
10
+
11
+ - **RubyGems:** [`google-maps-scraper-sdk`](https://rubygems.org/gems/google-maps-scraper-sdk)
12
+ - **Require:** `gmaps_scraper` → `GmapsScraper::Client`
13
+ - **CLI:** `gmaps-scraper`
14
+
15
+ ## Get an API key
16
+
17
+ 1. Sign in at [gmapsleadfinder.com](https://gmapsleadfinder.com).
18
+ 2. Use a plan with Agent API (**Growth** or higher) — see [Pricing](https://gmapsleadfinder.com/pricing).
19
+ 3. Open [Account → API key](https://gmapsleadfinder.com/account#api-key) and copy your `gmf_…` key.
20
+
21
+ ```bash
22
+ export GMF_API_KEY=gmf_your_key_here
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ gem install google-maps-scraper-sdk
29
+ # or: bundle add google-maps-scraper-sdk
30
+ ```
31
+
32
+ Requires **Ruby 3.1+** (stdlib `net/http` + `json` only).
33
+
34
+ ## Quickstart
35
+
36
+ ```ruby
37
+ require "gmaps_scraper"
38
+
39
+ client = GmapsScraper::Client.new # reads GMF_API_KEY
40
+ me = client.me
41
+ puts "#{me['plan']} #{me['creditsRemaining']}"
42
+
43
+ rows = client.scrape("dentists in Austin TX")
44
+ puts rows.length
45
+ ```
46
+
47
+ CLI:
48
+
49
+ ```bash
50
+ gmaps-scraper me
51
+ gmaps-scraper scrape "dentists in Austin TX" --out leads.json
52
+ ```
53
+
54
+ From this directory:
55
+
56
+ ```bash
57
+ bundle install
58
+ bundle exec ruby examples/quickstart.rb
59
+ bundle exec gmaps-scraper me
60
+ ```
61
+
62
+ ## Client API
63
+
64
+ | Method | Description |
65
+ |--------|-------------|
66
+ | `Client.new(api_key:, base_url:, timeout_s:)` | From env / kwargs |
67
+ | `me` | Plan & credits |
68
+ | `create_job(keyword)` | Queue one-keyword job |
69
+ | `get_job(job_id)` | Poll status |
70
+ | `get_results(job_id, limit:, cursor:)` | Paginated rows |
71
+ | `scrape(keyword, poll_interval_ms:, timeout_ms:, result_limit:)` | Create → poll → all rows |
72
+
73
+ `scrape` defaults: `poll_interval_ms` 2000, `timeout_ms` 600000, `result_limit` 100.
74
+
75
+ ## Errors
76
+
77
+ | HTTP | Class |
78
+ |------|-------|
79
+ | 400 | `GmapsScraper::BadRequestError` |
80
+ | 401 | `GmapsScraper::AuthenticationError` |
81
+ | 402 | `GmapsScraper::InsufficientCreditsError` |
82
+ | 403 | `GmapsScraper::PlanNotAllowedError` |
83
+ | 404 | `GmapsScraper::NotFoundError` |
84
+ | 409 | `GmapsScraper::JobConflictError` |
85
+ | — | `GmapsScraper::TimeoutError`, `GmapsScraper::ApiError` |
86
+
87
+ ## Limits
88
+
89
+ - Exactly one keyword per API job
90
+ - One running job per user (`409` if busy)
91
+ - 1 credit = 1 place row; Growth+ required
92
+
93
+ ## Publish to RubyGems
94
+
95
+ ```bash
96
+ cd ruby
97
+ gem build google-maps-scraper-sdk.gemspec
98
+ gem push google-maps-scraper-sdk-0.1.1.gem
99
+ ```
100
+
101
+ ## Links
102
+
103
+ | Resource | URL |
104
+ |----------|-----|
105
+ | Website | https://gmapsleadfinder.com |
106
+ | HTTP API | https://gmapsleadfinder.com/docs/api |
107
+ | RubyGems | https://rubygems.org/gems/google-maps-scraper-sdk |
108
+ | Monorepo | https://github.com/google-maps-lead-scraper/google-maps-scraper |
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ require "gmaps_scraper"
5
+
6
+ client = GmapsScraper::Client.new
7
+ me = client.me
8
+ puts "plan=#{me['plan']} creditsRemaining=#{me['creditsRemaining']}"
9
+
10
+ rows = client.scrape("dentists in Austin TX")
11
+ puts "scraped #{rows.length} places"
12
+ unless rows.empty?
13
+ r = rows.first
14
+ puts "#{r['Name']} #{r['Phone']} #{r['Website']} #{r['Emails']}"
15
+ end
data/exe/gmaps-scraper ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "csv"
5
+ require "json"
6
+ require "optparse"
7
+ require "pathname"
8
+
9
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
10
+ require "gmaps_scraper"
11
+
12
+ def write_output(rows, out)
13
+ if out.nil?
14
+ puts JSON.pretty_generate(rows)
15
+ return
16
+ end
17
+
18
+ path = Pathname.new(out)
19
+ path.dirname.mkpath
20
+ suffix = path.extname.downcase
21
+ if suffix == ".csv"
22
+ fieldnames = []
23
+ seen = {}
24
+ rows.each do |row|
25
+ row.each_key do |key|
26
+ next if seen[key]
27
+
28
+ seen[key] = true
29
+ fieldnames << key
30
+ end
31
+ end
32
+ fieldnames = ["Name"] if fieldnames.empty?
33
+ CSV.open(path, "w") do |csv|
34
+ csv << fieldnames
35
+ rows.each do |row|
36
+ csv << fieldnames.map { |k| row[k] || "" }
37
+ end
38
+ end
39
+ else
40
+ path.write("#{JSON.pretty_generate(rows)}\n")
41
+ end
42
+ warn "Wrote #{rows.length} rows to #{path}"
43
+ end
44
+
45
+ def print_help
46
+ puts <<~HELP
47
+ Usage: gmaps-scraper <command> [options]
48
+
49
+ Commands:
50
+ me Show plan and credits
51
+ scrape "<keyword>" [opts] Scrape one keyword and print/save rows
52
+
53
+ Options for scrape:
54
+ --out PATH Output path (.json or .csv). Default: JSON to stdout
55
+ --poll-interval-ms MS Job poll interval (default: 2000)
56
+ --timeout-ms MS Max wait for job completion (default: 600000)
57
+
58
+ Global:
59
+ --version Print version
60
+ -h, --help Show this help
61
+ HELP
62
+ end
63
+
64
+ argv = ARGV.dup
65
+ if argv.empty? || argv.include?("-h") || argv.include?("--help")
66
+ print_help
67
+ exit(argv.empty? ? 1 : 0)
68
+ end
69
+ if argv.include?("--version")
70
+ puts GmapsScraper::VERSION
71
+ exit 0
72
+ end
73
+
74
+ cmd = argv.shift
75
+ begin
76
+ case cmd
77
+ when "me"
78
+ client = GmapsScraper::Client.new
79
+ puts JSON.pretty_generate(client.me)
80
+ when "scrape"
81
+ out = nil
82
+ poll_interval_ms = GmapsScraper::Client::DEFAULT_POLL_INTERVAL_MS
83
+ timeout_ms = GmapsScraper::Client::DEFAULT_TIMEOUT_MS
84
+ keyword = nil
85
+
86
+ OptionParser.new do |opts|
87
+ opts.on("--out PATH") { |v| out = v }
88
+ opts.on("--poll-interval-ms MS", Integer) { |v| poll_interval_ms = v }
89
+ opts.on("--timeout-ms MS", Integer) { |v| timeout_ms = v }
90
+ end.parse!(argv)
91
+
92
+ keyword = argv.shift
93
+ raise ArgumentError, "keyword is required" if keyword.nil? || keyword.strip.empty?
94
+
95
+ client = GmapsScraper::Client.new
96
+ rows = client.scrape(
97
+ keyword,
98
+ poll_interval_ms: poll_interval_ms,
99
+ timeout_ms: timeout_ms
100
+ )
101
+ write_output(rows, out)
102
+ else
103
+ raise ArgumentError, "unknown command #{cmd}"
104
+ end
105
+ rescue GmapsScraper::ApiError => e
106
+ warn "error: #{e.message}"
107
+ warn "status: #{e.status_code}" if e.status_code
108
+ exit 1
109
+ rescue ArgumentError => e
110
+ warn "error: #{e.message}"
111
+ exit 1
112
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/gmaps_scraper/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "google-maps-scraper-sdk"
7
+ spec.version = GmapsScraper::VERSION
8
+ spec.authors = ["GMaps Lead Finder"]
9
+ spec.email = ["support@gmapsleadfinder.com"]
10
+
11
+ spec.summary = "Google Maps Scraper / Extractor / Lead Scraper SDK for GMaps Lead Finder"
12
+ spec.description =
13
+ "Official Ruby client for the GMaps Lead Finder Agent HTTP API — scrape Google Maps " \
14
+ "places (name, phone, website, emails) via a hosted cloud pipeline."
15
+ spec.homepage = "https://gmapsleadfinder.com"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.1.0"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] =
21
+ "https://github.com/google-maps-lead-scraper/google-maps-scraper/tree/main/ruby"
22
+ spec.metadata["changelog_uri"] =
23
+ "https://github.com/google-maps-lead-scraper/google-maps-scraper/blob/main/CHANGELOG.md"
24
+ spec.metadata["documentation_uri"] = "https://gmapsleadfinder.com/docs/api"
25
+ spec.metadata["bug_tracker_uri"] =
26
+ "https://github.com/google-maps-lead-scraper/google-maps-scraper/issues"
27
+ spec.metadata["rubygems_mfa_required"] = "true"
28
+
29
+ spec.files = Dir.chdir(__dir__) do
30
+ Dir[
31
+ "lib/**/*",
32
+ "exe/*",
33
+ "examples/**/*",
34
+ "README.md",
35
+ "LICENSE",
36
+ "google-maps-scraper-sdk.gemspec"
37
+ ].select { |f| File.file?(f) }
38
+ end
39
+ spec.bindir = "exe"
40
+ spec.executables = ["gmaps-scraper"]
41
+ spec.require_paths = ["lib"]
42
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module GmapsScraper
8
+ class Client
9
+ DEFAULT_BASE_URL = "https://gmapsleadfinder.com"
10
+ DEFAULT_POLL_INTERVAL_MS = 2000
11
+ DEFAULT_TIMEOUT_MS = 600_000
12
+ DEFAULT_RESULT_LIMIT = 100
13
+ DEFAULT_HTTP_TIMEOUT_S = 60.0
14
+ USER_AGENT = "gmaps-scraper-ruby/#{VERSION}"
15
+ TERMINAL_STATUSES = %w[completed partial failed].freeze
16
+
17
+ def initialize(api_key: nil, base_url: nil, timeout_s: DEFAULT_HTTP_TIMEOUT_S)
18
+ key = (api_key || ENV["GMF_API_KEY"]).to_s.strip
19
+ if key.empty?
20
+ raise AuthenticationError,
21
+ "Missing API key. Set GMF_API_KEY or pass api_key: " \
22
+ "(https://gmapsleadfinder.com/account#api-key)"
23
+ end
24
+
25
+ @api_key = key
26
+ @base_url = (base_url || ENV["GMF_BASE_URL"] || DEFAULT_BASE_URL).to_s.strip.gsub(%r{/+\z}, "")
27
+ @timeout_s = timeout_s
28
+ end
29
+
30
+ def me
31
+ request("GET", "/api/v1/me")
32
+ end
33
+
34
+ def create_job(keyword)
35
+ trimmed = keyword.to_s.strip
36
+ raise BadRequestError, "keyword is required" if trimmed.empty?
37
+
38
+ request("POST", "/api/v1/jobs", json_body: { "keyword" => trimmed })
39
+ end
40
+
41
+ def get_job(job_id)
42
+ request("GET", "/api/v1/jobs/#{job_id}")
43
+ end
44
+
45
+ def get_results(job_id, limit: DEFAULT_RESULT_LIMIT, cursor: nil)
46
+ query = { "limit" => limit.to_s }
47
+ query["cursor"] = cursor.to_s unless cursor.nil?
48
+ request("GET", "/api/v1/jobs/#{job_id}/results", query: query)
49
+ end
50
+
51
+ def scrape(
52
+ keyword,
53
+ poll_interval_ms: DEFAULT_POLL_INTERVAL_MS,
54
+ timeout_ms: DEFAULT_TIMEOUT_MS,
55
+ result_limit: DEFAULT_RESULT_LIMIT
56
+ )
57
+ created = create_job(keyword)
58
+ job_id = created["jobId"].to_s
59
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (timeout_ms / 1000.0)
60
+ poll_s = [poll_interval_ms, 100].max / 1000.0
61
+
62
+ loop do
63
+ job = get_job(job_id)
64
+ status = job["status"].to_s.downcase
65
+ if TERMINAL_STATUSES.include?(status)
66
+ if status == "failed"
67
+ raise ApiError.new(job["error"] || "Job #{job_id} failed", body: job)
68
+ end
69
+ break
70
+ end
71
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
72
+ raise TimeoutError,
73
+ "Timed out after #{timeout_ms}ms waiting for job #{job_id} " \
74
+ "(last status=#{job['status'].inspect})"
75
+ end
76
+ sleep(poll_s)
77
+ end
78
+
79
+ fetch_all_rows(job_id, result_limit: result_limit)
80
+ end
81
+
82
+ private
83
+
84
+ def fetch_all_rows(job_id, result_limit:)
85
+ rows = []
86
+ cursor = "0"
87
+ while cursor
88
+ page = get_results(job_id, limit: result_limit, cursor: cursor)
89
+ rows.concat(Array(page["rows"]))
90
+ next_cursor = page["nextCursor"]
91
+ cursor = next_cursor.nil? || next_cursor == "" ? nil : next_cursor.to_s
92
+ end
93
+ rows
94
+ end
95
+
96
+ def request(method, path, json_body: nil, query: nil)
97
+ uri = URI("#{@base_url}#{path}")
98
+ if query && !query.empty?
99
+ uri.query = URI.encode_www_form(query)
100
+ end
101
+
102
+ http = Net::HTTP.new(uri.host, uri.port)
103
+ http.use_ssl = uri.scheme == "https"
104
+ http.open_timeout = @timeout_s
105
+ http.read_timeout = @timeout_s
106
+
107
+ klass = case method.upcase
108
+ when "GET" then Net::HTTP::Get
109
+ when "POST" then Net::HTTP::Post
110
+ when "PUT" then Net::HTTP::Put
111
+ when "DELETE" then Net::HTTP::Delete
112
+ else
113
+ raise ApiError, "Unsupported HTTP method #{method}"
114
+ end
115
+
116
+ req = klass.new(uri)
117
+ req["Authorization"] = "Bearer #{@api_key}"
118
+ req["Accept"] = "application/json"
119
+ req["User-Agent"] = USER_AGENT
120
+ if json_body
121
+ req["Content-Type"] = "application/json"
122
+ req.body = JSON.generate(json_body)
123
+ end
124
+
125
+ begin
126
+ res = http.request(req)
127
+ rescue StandardError => e
128
+ raise ApiError, "Network error: #{e.message}"
129
+ end
130
+
131
+ body = parse_body(res.body)
132
+ code = res.code.to_i
133
+ raise_for_status(code, body) if code < 200 || code >= 300
134
+ body.is_a?(Hash) ? body : {}
135
+ end
136
+
137
+ def parse_body(raw)
138
+ return {} if raw.nil? || raw.empty?
139
+
140
+ JSON.parse(raw)
141
+ rescue JSON::ParserError
142
+ { "message" => raw }
143
+ end
144
+
145
+ def raise_for_status(status, body)
146
+ message = "API request failed with status #{status}"
147
+ if body.is_a?(Hash)
148
+ %w[message error statusMessage].each do |key|
149
+ val = body[key]
150
+ if val.is_a?(String) && !val.empty?
151
+ message = val
152
+ break
153
+ end
154
+ end
155
+ end
156
+
157
+ klass = case status
158
+ when 400 then BadRequestError
159
+ when 401 then AuthenticationError
160
+ when 402 then InsufficientCreditsError
161
+ when 403 then PlanNotAllowedError
162
+ when 404 then NotFoundError
163
+ when 409 then JobConflictError
164
+ else ApiError
165
+ end
166
+ raise klass.new(message, status_code: status, body: body)
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GmapsScraper
4
+ class ApiError < StandardError
5
+ attr_reader :status_code, :body
6
+
7
+ def initialize(message, status_code: nil, body: nil)
8
+ super(message)
9
+ @status_code = status_code
10
+ @body = body
11
+ end
12
+ end
13
+
14
+ class AuthenticationError < ApiError; end
15
+ class PlanNotAllowedError < ApiError; end
16
+ class InsufficientCreditsError < ApiError; end
17
+ class JobConflictError < ApiError; end
18
+ class NotFoundError < ApiError; end
19
+ class BadRequestError < ApiError; end
20
+ class TimeoutError < ApiError; end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GmapsScraper
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gmaps_scraper/version"
4
+ require_relative "gmaps_scraper/errors"
5
+ require_relative "gmaps_scraper/client"
6
+
7
+ module GmapsScraper
8
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: google-maps-scraper-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - GMaps Lead Finder
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-09-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Official Ruby client for the GMaps Lead Finder Agent HTTP API — scrape
14
+ Google Maps places (name, phone, website, emails) via a hosted cloud pipeline.
15
+ email:
16
+ - support@gmapsleadfinder.com
17
+ executables:
18
+ - gmaps-scraper
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - examples/quickstart.rb
25
+ - exe/gmaps-scraper
26
+ - google-maps-scraper-sdk.gemspec
27
+ - lib/gmaps_scraper.rb
28
+ - lib/gmaps_scraper/client.rb
29
+ - lib/gmaps_scraper/errors.rb
30
+ - lib/gmaps_scraper/version.rb
31
+ homepage: https://gmapsleadfinder.com
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ homepage_uri: https://gmapsleadfinder.com
36
+ source_code_uri: https://github.com/google-maps-lead-scraper/google-maps-scraper/tree/main/ruby
37
+ changelog_uri: https://github.com/google-maps-lead-scraper/google-maps-scraper/blob/main/CHANGELOG.md
38
+ documentation_uri: https://gmapsleadfinder.com/docs/api
39
+ bug_tracker_uri: https://github.com/google-maps-lead-scraper/google-maps-scraper/issues
40
+ rubygems_mfa_required: 'true'
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 3.1.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.0.3.1
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Google Maps Scraper / Extractor / Lead Scraper SDK for GMaps Lead Finder
60
+ test_files: []