ipdata 0.1.5

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: ecd1e1860fb3d422c8aea50274e10528dd45d12c4555de9585e7af64f06de450
4
+ data.tar.gz: f7ad0dcf1f3fd4a526e5ebe64b901c848f4c5cab432ebe33d0a897f72f9c500d
5
+ SHA512:
6
+ metadata.gz: 5c3e379a60cd06fc5a5e38aca4680d2014c6b2324bd2587cb108310c0c1356cfc294bd2082ed1d3278e4b7950e5df1c0ff0d7da89be6f40d2b307e63eebdf0e9
7
+ data.tar.gz: 18f91231a4c26f1c3ba5f2e86436eff0ca22a2b8078f9749af994ace74de4707c4c95d0416344ed658ca186504bdbb3ebfbdec669466ce00d8c6c1a095ad8377
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 IPData
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,235 @@
1
+ # IPData Ruby SDK
2
+
3
+ Official Ruby client for the [ipdata.co](https://ipdata.co) IP geolocation API. Look up geolocation, threat intelligence, ASN, company, carrier, currency, timezone, and language data for any IP address.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "ipdata"
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```sh
16
+ bundle install
17
+ ```
18
+
19
+ Or install directly:
20
+
21
+ ```sh
22
+ gem install ipdata
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ Get a free API key at [ipdata.co/sign-up](https://ipdata.co/sign-up.html) (1,500 requests/day).
28
+
29
+ ```ruby
30
+ require "ipdata"
31
+
32
+ client = IPData::Client.new("YOUR_API_KEY")
33
+
34
+ # Look up an IP address
35
+ response = client.lookup("8.8.8.8")
36
+
37
+ response.ip # => "8.8.8.8"
38
+ response.city # => "Mountain View"
39
+ response.country_name # => "United States"
40
+ response.latitude # => 37.386
41
+ response.longitude # => -122.0838
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Look Up Your Own IP
47
+
48
+ ```ruby
49
+ response = client.lookup
50
+ response.ip # => your public IP
51
+ ```
52
+
53
+ ### Look Up a Specific IP
54
+
55
+ ```ruby
56
+ response = client.lookup("1.1.1.1")
57
+ response.country_name # => "Australia"
58
+ response.asn.name # => "Cloudflare, Inc."
59
+ response.asn.asn # => "AS13335"
60
+ ```
61
+
62
+ ### Select Specific Fields
63
+
64
+ Reduce payload by requesting only the fields you need:
65
+
66
+ ```ruby
67
+ response = client.lookup("8.8.8.8", fields: ["ip", "city", "country_name"])
68
+ response.city # => "Mountain View"
69
+ ```
70
+
71
+ ### Extract a Single Field
72
+
73
+ ```ruby
74
+ city = client.lookup("8.8.8.8", select_field: "city")
75
+ # => "Mountain View"
76
+ ```
77
+
78
+ ### Bulk Lookup
79
+
80
+ Look up multiple IPs in a single request (max 100):
81
+
82
+ ```ruby
83
+ responses = client.bulk_lookup(["8.8.8.8", "1.1.1.1"])
84
+ responses.each do |r|
85
+ puts "#{r.ip}: #{r.country_name}"
86
+ end
87
+ ```
88
+
89
+ Bulk lookup also supports field filtering:
90
+
91
+ ```ruby
92
+ responses = client.bulk_lookup(
93
+ ["8.8.8.8", "1.1.1.1"],
94
+ fields: ["ip", "country_name", "city"]
95
+ )
96
+ ```
97
+
98
+ ### EU Endpoint (GDPR Compliant)
99
+
100
+ Route requests through EU data centers only:
101
+
102
+ ```ruby
103
+ client = IPData::Client.eu("YOUR_API_KEY")
104
+ ```
105
+
106
+ ### Caching
107
+
108
+ Enable an in-memory LRU cache to reduce API calls:
109
+
110
+ ```ruby
111
+ cache = IPData::Cache.new(max_size: 1024, ttl: 3600) # 1h TTL
112
+ client = IPData::Client.new("YOUR_API_KEY", cache: cache)
113
+
114
+ client.lookup("8.8.8.8") # hits API
115
+ client.lookup("8.8.8.8") # served from cache
116
+ ```
117
+
118
+ ### Custom Timeout
119
+
120
+ ```ruby
121
+ client = IPData::Client.new("YOUR_API_KEY", timeout: 10) # 10 seconds
122
+ ```
123
+
124
+ ## Response Fields
125
+
126
+ All responses are wrapped in `IPData::Response` objects that support both dot-notation and hash-style access:
127
+
128
+ ```ruby
129
+ response = client.lookup("8.8.8.8")
130
+
131
+ # Dot notation
132
+ response.ip
133
+ response.city
134
+ response.region
135
+ response.country_name
136
+ response.country_code
137
+ response.continent_name
138
+ response.latitude
139
+ response.longitude
140
+ response.is_eu
141
+ response.postal
142
+ response.calling_code
143
+ response.flag
144
+ response.emoji_flag
145
+ response.emoji_unicode
146
+
147
+ # Nested objects
148
+ response.asn.asn # => "AS15169"
149
+ response.asn.name # => "Google LLC"
150
+ response.asn.domain # => "google.com"
151
+ response.asn.route # => "8.8.8.0/24"
152
+ response.asn.type # => "business"
153
+
154
+ response.company.name
155
+ response.company.domain
156
+ response.company.network
157
+ response.company.type
158
+
159
+ response.carrier.name
160
+ response.carrier.mcc
161
+ response.carrier.mnc
162
+
163
+ response.currency.name
164
+ response.currency.code
165
+ response.currency.symbol
166
+ response.currency.native
167
+ response.currency.plural
168
+
169
+ response.time_zone.name
170
+ response.time_zone.abbr
171
+ response.time_zone.offset
172
+ response.time_zone.is_dst
173
+ response.time_zone.current_time
174
+
175
+ response.threat.is_tor
176
+ response.threat.is_vpn
177
+ response.threat.is_icloud_relay
178
+ response.threat.is_proxy
179
+ response.threat.is_datacenter
180
+ response.threat.is_anonymous
181
+ response.threat.is_known_attacker
182
+ response.threat.is_known_abuser
183
+ response.threat.is_threat
184
+ response.threat.is_bogon
185
+ response.threat.blocklists
186
+ response.threat.scores
187
+
188
+ # Languages (array)
189
+ response.languages.first.name
190
+ response.languages.first.native
191
+ response.languages.first.code
192
+
193
+ # Hash-style access
194
+ response["city"]
195
+ response[:country_code]
196
+
197
+ # Raw hash
198
+ response.to_h
199
+ ```
200
+
201
+ ## Error Handling
202
+
203
+ ```ruby
204
+ begin
205
+ client.lookup("8.8.8.8")
206
+ rescue IPData::AuthenticationError => e
207
+ # Missing API key (401)
208
+ puts "Auth error: #{e.message} (#{e.status_code})"
209
+ rescue IPData::ForbiddenError => e
210
+ # Invalid key or quota exceeded (403)
211
+ puts "Forbidden: #{e.message} (#{e.status_code})"
212
+ rescue IPData::BadRequestError => e
213
+ # Invalid IP or bad request (400)
214
+ puts "Bad request: #{e.message} (#{e.status_code})"
215
+ rescue IPData::Error => e
216
+ # Any other API error
217
+ puts "Error: #{e.message} (#{e.status_code})"
218
+ end
219
+ ```
220
+
221
+ ## Requirements
222
+
223
+ - Ruby >= 3.0
224
+ - No runtime dependencies (uses only Ruby stdlib)
225
+
226
+ ## Development
227
+
228
+ ```sh
229
+ bundle install
230
+ bundle exec rake test
231
+ ```
232
+
233
+ ## License
234
+
235
+ [MIT](LICENSE)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module IPData
6
+ # Thread-safe LRU cache with TTL expiration.
7
+ #
8
+ # Used to cache IP lookup results and avoid redundant API calls.
9
+ # Uses Ruby's stdlib Monitor for thread safety.
10
+ #
11
+ # @example
12
+ # cache = IPData::Cache.new(max_size: 1024, ttl: 3600)
13
+ # client = IPData::Client.new("KEY", cache: cache)
14
+ class Cache
15
+ Entry = Struct.new(:value, :expires_at)
16
+
17
+ DEFAULT_MAX_SIZE = 4096
18
+ DEFAULT_TTL = 86_400 # 24 hours
19
+
20
+ def initialize(max_size: DEFAULT_MAX_SIZE, ttl: DEFAULT_TTL)
21
+ @max_size = max_size
22
+ @ttl = ttl
23
+ @store = {}
24
+ @monitor = Monitor.new
25
+ end
26
+
27
+ # Retrieve a cached value. Returns nil if missing or expired.
28
+ def get(key)
29
+ @monitor.synchronize do
30
+ entry = @store[key]
31
+ return nil unless entry
32
+
33
+ if Time.now > entry.expires_at
34
+ @store.delete(key)
35
+ return nil
36
+ end
37
+
38
+ # Move to end (most recently used) by re-inserting
39
+ @store.delete(key)
40
+ @store[key] = entry
41
+ entry.value
42
+ end
43
+ end
44
+
45
+ # Store a value in the cache. Evicts the least recently used entry
46
+ # if the cache is at capacity.
47
+ def set(key, value)
48
+ @monitor.synchronize do
49
+ @store.delete(key) # remove if exists (re-insert at end)
50
+
51
+ # Evict LRU entry if at capacity
52
+ if @store.size >= @max_size
53
+ lru_key = @store.keys.first
54
+ @store.delete(lru_key)
55
+ end
56
+
57
+ @store[key] = Entry.new(value, Time.now + @ttl)
58
+ end
59
+ end
60
+
61
+ # Remove all entries.
62
+ def clear
63
+ @monitor.synchronize { @store.clear }
64
+ end
65
+
66
+ # Number of entries currently cached.
67
+ def size
68
+ @monitor.synchronize { @store.size }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module IPData
8
+ # HTTP client for the ipdata.co API.
9
+ #
10
+ # @example Basic usage
11
+ # client = IPData::Client.new("YOUR_API_KEY")
12
+ # response = client.lookup("8.8.8.8")
13
+ # response.city # => "Mountain View"
14
+ #
15
+ # @example EU endpoint (GDPR compliant)
16
+ # client = IPData::Client.eu("YOUR_API_KEY")
17
+ #
18
+ # @example With caching
19
+ # cache = IPData::Cache.new(max_size: 1024, ttl: 3600)
20
+ # client = IPData::Client.new("YOUR_API_KEY", cache: cache)
21
+ class Client
22
+ BASE_URL = "https://api.ipdata.co"
23
+ EU_BASE_URL = "https://eu-api.ipdata.co"
24
+
25
+ # @param api_key [String] Your ipdata API key
26
+ # @param cache [IPData::Cache, nil] Optional LRU cache instance
27
+ # @param timeout [Integer] HTTP timeout in seconds (default: 30)
28
+ # @param base_url [String, nil] Override the base URL (default: global endpoint)
29
+ def initialize(api_key, cache: nil, timeout: 30, base_url: nil)
30
+ raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
31
+
32
+ @api_key = api_key
33
+ @cache = cache
34
+ @timeout = timeout
35
+ @base_uri = URI.parse(base_url || BASE_URL)
36
+ end
37
+
38
+ # Create a client configured for the EU endpoint.
39
+ #
40
+ # @param api_key [String] Your ipdata API key
41
+ # @param opts [Hash] Additional options passed to {#initialize}
42
+ # @return [Client]
43
+ def self.eu(api_key, **opts)
44
+ new(api_key, base_url: EU_BASE_URL, **opts)
45
+ end
46
+
47
+ # Look up geolocation data for an IP address.
48
+ #
49
+ # @param ip [String, nil] IP address to look up (nil for caller's own IP)
50
+ # @param fields [Array<String>, nil] Specific fields to return
51
+ # @param select_field [String, nil] A single field to extract (returns raw value)
52
+ # @return [Response, Object] Response object, or raw value if select_field is used
53
+ def lookup(ip = nil, fields: nil, select_field: nil)
54
+ if select_field
55
+ path = "/#{ip}/#{select_field}"
56
+ data = get(path)
57
+ return data
58
+ end
59
+
60
+ cache_key = build_cache_key(ip, fields)
61
+ if @cache
62
+ cached = @cache.get(cache_key)
63
+ return cached if cached
64
+ end
65
+
66
+ path = ip ? "/#{ip}" : "/"
67
+ params = {}
68
+ params["fields"] = fields.join(",") if fields && !fields.empty?
69
+
70
+ data = get(path, params)
71
+ response = Response.new(data)
72
+
73
+ @cache&.set(cache_key, response)
74
+ response
75
+ end
76
+
77
+ # Look up geolocation data for multiple IP addresses.
78
+ #
79
+ # @param ips [Array<String>] IP addresses (max 100)
80
+ # @param fields [Array<String>, nil] Specific fields to return
81
+ # @return [Array<Response>]
82
+ # @raise [ArgumentError] if more than 100 IPs are provided
83
+ def bulk_lookup(ips, fields: nil)
84
+ raise ArgumentError, "ips must be an Array" unless ips.is_a?(Array)
85
+ raise ArgumentError, "maximum 100 IPs per bulk request" if ips.size > 100
86
+
87
+ params = {}
88
+ params["fields"] = fields.join(",") if fields && !fields.empty?
89
+
90
+ data = post("/bulk", ips, params)
91
+ data.map { |entry| Response.new(entry) }
92
+ end
93
+
94
+ private
95
+
96
+ def user_agent
97
+ @user_agent ||= "ruby-ipdata/#{VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
98
+ end
99
+
100
+ def build_cache_key(ip, fields)
101
+ key = ip || "_self"
102
+ key = "#{key}:#{fields.sort.join(",")}" if fields && !fields.empty?
103
+ key
104
+ end
105
+
106
+ def build_uri(path, params = {})
107
+ uri = @base_uri.dup
108
+ uri.path = path
109
+ query_params = { "api-key" => @api_key }.merge(params)
110
+ uri.query = URI.encode_www_form(query_params)
111
+ uri
112
+ end
113
+
114
+ def get(path, params = {})
115
+ uri = build_uri(path, params)
116
+
117
+ request = Net::HTTP::Get.new(uri)
118
+ request["Accept"] = "application/json"
119
+ request["User-Agent"] = user_agent
120
+
121
+ execute(uri, request)
122
+ end
123
+
124
+ def post(path, body, params = {})
125
+ uri = build_uri(path, params)
126
+
127
+ request = Net::HTTP::Post.new(uri)
128
+ request["Accept"] = "application/json"
129
+ request["Content-Type"] = "application/json"
130
+ request["User-Agent"] = user_agent
131
+ request.body = JSON.generate(body)
132
+
133
+ execute(uri, request)
134
+ end
135
+
136
+ def execute(uri, request)
137
+ http = Net::HTTP.new(uri.host, uri.port)
138
+ http.use_ssl = true
139
+ http.open_timeout = @timeout
140
+ http.read_timeout = @timeout
141
+
142
+ response = http.request(request)
143
+
144
+ unless response.is_a?(Net::HTTPSuccess)
145
+ raise Error.from_response(response.code.to_i, response.body)
146
+ end
147
+
148
+ JSON.parse(response.body)
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module IPData
6
+ # Base error class for all ipdata API errors.
7
+ # Carries the HTTP status code and API error message.
8
+ class Error < StandardError
9
+ attr_reader :status_code
10
+
11
+ def initialize(message, status_code = nil)
12
+ @status_code = status_code
13
+ super(message)
14
+ end
15
+
16
+ # Build the appropriate error subclass from an HTTP response.
17
+ def self.from_response(status, body)
18
+ message = parse_message(body) || "API request failed"
19
+ klass = case status
20
+ when 400 then BadRequestError
21
+ when 401 then AuthenticationError
22
+ when 403 then ForbiddenError
23
+ else Error
24
+ end
25
+ klass.new(message, status)
26
+ end
27
+
28
+ def self.parse_message(body)
29
+ return nil if body.nil? || body.empty?
30
+
31
+ data = JSON.parse(body)
32
+ data["message"]
33
+ rescue JSON::ParserError
34
+ body
35
+ end
36
+ private_class_method :parse_message
37
+ end
38
+
39
+ # Raised when the API key is missing (HTTP 401).
40
+ class AuthenticationError < Error; end
41
+
42
+ # Raised when the API key is invalid or quota is exceeded (HTTP 403).
43
+ class ForbiddenError < Error; end
44
+
45
+ # Raised for invalid IP addresses or malformed requests (HTTP 400).
46
+ class BadRequestError < Error; end
47
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module IPData
6
+ # Lightweight wrapper around the parsed API JSON response.
7
+ #
8
+ # Provides both dot-notation and hash-style access:
9
+ # response.city # => "Mountain View"
10
+ # response["city"] # => "Mountain View"
11
+ # response[:city] # => "Mountain View"
12
+ # response.asn.name # => "Google LLC"
13
+ # response.threat.is_vpn # => false
14
+ #
15
+ # Nested hashes are recursively wrapped as Response objects.
16
+ # Arrays of hashes (e.g. languages, blocklists) are mapped to Response arrays.
17
+ class Response
18
+ def initialize(data)
19
+ @data = data || {}
20
+ end
21
+
22
+ # Hash-style access. Accepts string or symbol keys.
23
+ def [](key)
24
+ wrap(@data[key.to_s])
25
+ end
26
+
27
+ # Returns the underlying raw Hash.
28
+ def to_h
29
+ @data
30
+ end
31
+
32
+ # Serialize back to JSON.
33
+ def to_json(*)
34
+ JSON.generate(@data)
35
+ end
36
+
37
+ def to_s
38
+ @data.to_s
39
+ end
40
+
41
+ def inspect
42
+ "#<#{self.class} #{@data.inspect}>"
43
+ end
44
+
45
+ # Returns true if the key exists in the response data.
46
+ def key?(key)
47
+ @data.key?(key.to_s)
48
+ end
49
+
50
+ # Iterate over key-value pairs.
51
+ def each(&block)
52
+ @data.each(&block)
53
+ end
54
+
55
+ def ==(other)
56
+ case other
57
+ when Response then @data == other.to_h
58
+ when Hash then @data == other
59
+ else false
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def wrap(value)
66
+ case value
67
+ when Hash then Response.new(value)
68
+ when Array then value.map { |v| wrap(v) }
69
+ else value
70
+ end
71
+ end
72
+
73
+ def method_missing(name, *args)
74
+ key = name.to_s
75
+ if @data.key?(key)
76
+ wrap(@data[key])
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ def respond_to_missing?(name, include_private = false)
83
+ @data.key?(name.to_s) || super
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IPData
4
+ VERSION = "0.1.5"
5
+ end
data/lib/ipdata.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ipdata/version"
4
+ require_relative "ipdata/errors"
5
+ require_relative "ipdata/response"
6
+ require_relative "ipdata/cache"
7
+ require_relative "ipdata/client"
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ipdata
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - IPData
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Official Ruby SDK for the ipdata.co API. Look up geolocation, threat
14
+ intelligence, ASN, company, carrier, currency, timezone and language data for any
15
+ IP address.
16
+ email:
17
+ - support@ipdata.co
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - lib/ipdata.rb
25
+ - lib/ipdata/cache.rb
26
+ - lib/ipdata/client.rb
27
+ - lib/ipdata/errors.rb
28
+ - lib/ipdata/response.rb
29
+ - lib/ipdata/version.rb
30
+ homepage: https://github.com/ipdata/ruby
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ homepage_uri: https://github.com/ipdata/ruby
35
+ source_code_uri: https://github.com/ipdata/ruby
36
+ changelog_uri: https://github.com/ipdata/ruby/blob/main/CHANGELOG.md
37
+ bug_tracker_uri: https://github.com/ipdata/ruby/issues
38
+ documentation_uri: https://docs.ipdata.co
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.5.22
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Ruby client for the ipdata IP geolocation API
58
+ test_files: []