ip_api_service 0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 508fddf5e55811a7f08ae47583e4627631d925c6d6a1e91c3536bd457b3856e4
4
+ data.tar.gz: 7d302d15e4d3e6e5334d8fafccd9cbb9c4428babe520f9b17c3aa5679e1b277f
5
+ SHA512:
6
+ metadata.gz: 60516003088fa1217aa0e8fce5d106744efeecb1a2569ccc1d8076ddc746309a1652ed78d8e578a8755dc74b8a500e74308100196f3c10cec4ea7b629fc70eb7
7
+ data.tar.gz: 0a2390cfe4396ca468630c8f4fc1bb520612c9fdc8321197881991443c7764ff46929efb06789c5a9c42226a03ce5c5aa8893a605a037c5f411f411db640bd72
data/.rubocop.yml ADDED
@@ -0,0 +1,19 @@
1
+ AllCops:
2
+ Exclude:
3
+ - "**/vendor/**/*"
4
+ - "**/*.gemspec"
5
+ - "spec/**"
6
+ NewCops: enable
7
+ TargetRubyVersion: 3.0.1
8
+ SuggestExtensions: false
9
+
10
+ Style/Documentation:
11
+ Enabled: false
12
+ Metrics/MethodLength:
13
+ Enabled: false
14
+ Style/AsciiComments:
15
+ Enabled: false
16
+
17
+ # NOTE: for rspec tests
18
+ Metrics/BlockLength:
19
+ IgnoredMethods: ['describe', 'context']
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in ip_api_service.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'nokogiri-happymapper'
11
+
12
+ gem 'addressable'
13
+
14
+ group :develop do
15
+ gem 'minitest', '~> 5.0'
16
+ gem 'minitest-power_assert'
17
+ gem 'webmock'
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ip_api_service (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ addressable (2.8.0)
10
+ public_suffix (>= 2.0.2, < 5.0)
11
+ crack (0.4.5)
12
+ rexml
13
+ hashdiff (1.0.1)
14
+ minitest (5.15.0)
15
+ minitest-power_assert (0.3.1)
16
+ minitest
17
+ power_assert (>= 1.1)
18
+ nokogiri (1.13.3-x86_64-linux)
19
+ racc (~> 1.4)
20
+ nokogiri-happymapper (0.9.0)
21
+ nokogiri (~> 1.5)
22
+ power_assert (2.0.1)
23
+ public_suffix (4.0.6)
24
+ racc (1.6.0)
25
+ rake (13.0.6)
26
+ rexml (3.2.5)
27
+ webmock (3.14.0)
28
+ addressable (>= 2.8.0)
29
+ crack (>= 0.3.2)
30
+ hashdiff (>= 0.4.0, < 2.0.0)
31
+
32
+ PLATFORMS
33
+ x86_64-linux
34
+
35
+ DEPENDENCIES
36
+ addressable
37
+ ip_api_service!
38
+ minitest (~> 5.0)
39
+ minitest-power_assert
40
+ nokogiri-happymapper
41
+ rake (~> 13.0)
42
+ webmock
43
+
44
+ BUNDLED WITH
45
+ 2.3.7
data/Makefile ADDED
@@ -0,0 +1,8 @@
1
+ test:
2
+ test -s solution
3
+ install:
4
+ bundle install
5
+ tdd:
6
+ rake test
7
+
8
+ .PHONY: test
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # IpApiService
2
+
3
+ ![logo](https://ip-api.com/docs/static/logo.png)
4
+
5
+ Ruby клинет для сервиса [ip-api.com](https://ip-api.com/). Сервис позволяет получать данные геолокации по ip адресу.
6
+
7
+ ## Установка
8
+
9
+ ```ruby
10
+ gem 'ip-api-service'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ip-api-service
20
+
21
+ ## Использование
22
+
23
+ Вы можете получить данные геолокации ip адреса вызвав метод IpApiService.lookup и передав ему следующие параметры:
24
+
25
+ * ip - ip адрес
26
+ * fields - массив с нужными полями
27
+ * result_format - формат желаемого результата
28
+ * lang - язык
29
+
30
+ Результатом метода, при вызове с параметром result_format: :ipMetaInfo, будет объект.
31
+ Значения запрашиваемх полей будут доступны по геттеру с именем поля
32
+
33
+ ```ruby
34
+ info = IpApiService.lookup '8.8.8.8'
35
+ puts info.city
36
+ ```
37
+ Во всех других случаях результатом метода будет строка
38
+
39
+ Доступные для запроса поля можно получить вызвав метод available_fields
40
+
41
+ ```ruby
42
+ IpApiService.available_fields
43
+ ```
44
+
45
+ Поддерживаемые форматы результата - available_formats
46
+
47
+ ```ruby
48
+ IpApiService.available_formats
49
+ ```
50
+
51
+ Доступные языки - available_languages
52
+
53
+ ```ruby
54
+ IpApiService.available_languages
55
+ ```
56
+
57
+ Используя метод field_description(field) vожно получить описание поля
58
+
59
+ ```ruby
60
+ IpApiServiceIpApiService.field_description :region
61
+ ```
62
+
63
+ Параметр ip для метода lookup обязательный, остальные можно установить по умолчанию
64
+
65
+ ```ruby
66
+ IpApiService.default_fields = %i(city country countryCode lat lon)
67
+ IpApiService.result_format = :json
68
+ IpApiService.default_language = :en
69
+ ```
70
+
71
+ ## Development
72
+
73
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
74
+
75
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
76
+
77
+ ## Contributing
78
+
79
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mike090/ip_api_service.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ip_api_service/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ip_api_service"
7
+ spec.version = IpApiService::VERSION
8
+ spec.authors = ["mike09"]
9
+ spec.email = ["mike09@mail.ru"]
10
+
11
+ spec.summary = "API for ip-api.com"
12
+ spec.homepage = "https://github.com/mike090/ip_api_service"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/mike090/ip_api_service"
17
+ spec.metadata["changelog_uri"] = "https://github.com/mike090/ip_api_service"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ # spec.add_dependency "example-gem", "~> 1.0"
32
+
33
+ # For more information and examples about making a new gem, check out our
34
+ # guide at: https://bundler.io/guides/creating_gem.html
35
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../web_service/uri_mapper'
4
+ require_relative '../web_service/http_command'
5
+ require_relative 'ip_api_response_processor'
6
+
7
+ module IpApiService
8
+ # available meta fields
9
+ META_FIELDS = {
10
+ continent: { type: :string, description: 'Continent name' },
11
+ continentCode: { type: :string, description: 'Two-letter continent code' },
12
+ country: { type: :string, description: 'Country name' },
13
+ countryCode: { type: :string, description: 'Two-letter country code ISO 3166-1 alpha-2' },
14
+ region: { type: :string, description: 'Region/state short code (FIPS or ISO)' },
15
+ regionName: { type: :string, description: 'Region/state' },
16
+ city: { type: :string, description: 'City' },
17
+ district: { type: :string, description: 'District (subdivision of city)' },
18
+ zip: { type: :string, description: 'Zip code' },
19
+ lat: { type: :float, description: 'Latitude' },
20
+ lon: { type: :float, description: 'Longitude' },
21
+ timezone: { type: :string, description: 'Timezone (tz)' },
22
+ offset: { type: :integer, description: 'Timezone UTC DST offset in seconds' },
23
+ currency: { type: :string, description: 'National currency' },
24
+ isp: { type: :string, description: 'ISP name' },
25
+ org: { type: :string, description: 'Organization name' },
26
+ as: { type: :string,
27
+ description: "AS number and organization, separated by space (RIR). Empty for IP blocks \
28
+ 'not being announced in BGP tables." },
29
+ asname: { type: :string,
30
+ description: 'AS name (RIR). Empty for IP blocks not being announced in BGP tables' },
31
+ reverse: { type: :string, description: 'Reverse DNS of the IP (can delay response)' },
32
+ mobile: { type: :boolean, description: 'Mobile (cellular) connection' },
33
+ proxy: { type: :boolean, description: 'Proxy, VPN or Tor exit address' },
34
+ hosting: { type: :boolean, description: 'Hosting, colocated or data center' }
35
+ }.freeze
36
+ private_constant :META_FIELDS
37
+
38
+ # service fields
39
+ SERVICE_FIELDS = {
40
+ status: :string,
41
+ message: :string,
42
+ query: :string
43
+ }.freeze
44
+ private_constant :SERVICE_FIELDS
45
+
46
+ FIELD_TYPES = META_FIELDS.transform_values do |field_scheme|
47
+ field_scheme[:type]
48
+ end.merge(SERVICE_FIELDS).freeze
49
+ private_constant :FIELD_TYPES
50
+
51
+ IP_API_COMMAND_TEMPLATE = 'http://ip-api.com/{format}/{ip}{?fields}{&lang}'
52
+ private_constant :IP_API_COMMAND_TEMPLATE
53
+
54
+ USER_AGENT = 'IpApiService/Ruby/1.0'
55
+ private_constant :USER_AGENT
56
+
57
+ ACCEPT_MIME_TYPES = {
58
+ json: 'application/json',
59
+ xml: 'application/xml',
60
+ csv: 'text/csv',
61
+ newline: 'text/plain',
62
+ php: 'text/php'
63
+ }.freeze
64
+ private_constant :ACCEPT_MIME_TYPES
65
+
66
+ class IpApiAdapter
67
+ def initialize
68
+ mapper = WebService::UriMapper
69
+ @command = WebService::HttpCommand.new mapper
70
+ end
71
+
72
+ def ip_meta_info(ip, fields, result_format, lang)
73
+ target_fields = SERVICE_FIELDS.keys + fields
74
+ target_format = result_format == :ipMetaInfo ? :xml : result_format
75
+ headers = prepare_headers target_format
76
+ response = @command.execute :get, IP_API_COMMAND_TEMPLATE, ip: ip, format: target_format, fields: target_fields,
77
+ lang: lang, headers: headers
78
+ return response_processor.process_response(response, target_format, fields) if result_format == :ipMetaInfo
79
+
80
+ response.body
81
+ end
82
+
83
+ private
84
+
85
+ def response_processor
86
+ @response_processor ||= ResponseProcessor.new
87
+ end
88
+
89
+ def prepare_headers(format)
90
+ {
91
+ 'User-Agent' => USER_AGENT,
92
+ 'Accept' => ACCEPT_MIME_TYPES[format],
93
+ 'Accept-Encoding' => 'utf-8'
94
+ }
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../web_service/http_response_processor'
4
+ require_relative 'serialization'
5
+
6
+ module IpApiService
7
+ XML_RESPONSE_ROOT = 'query'
8
+ private_constant :XML_RESPONSE_ROOT
9
+
10
+ IP_API_SUCCESS_STATUS = 'success'
11
+ private_constant :IP_API_SUCCESS_STATUS
12
+
13
+ class ResponseProcessorError < StandardError; end
14
+
15
+ class ServiceError < StandardError
16
+ attr_reader :query, :service_message
17
+
18
+ def initialize(query, service_message)
19
+ @query = query
20
+ @service_message = service_message
21
+ super 'Service return fail result'
22
+ end
23
+ end
24
+
25
+ class ResponseProcessor < WebService::HttpResponseProcessor
26
+ def process_response(response, content_type, fields)
27
+ super response
28
+ ip_api_result = service_parser(content_type).parse response.body
29
+ unless ip_api_result.status == IP_API_SUCCESS_STATUS
30
+ raise ServiceError.new(ip_api_result.query,
31
+ ip_api_result.message)
32
+ end
33
+
34
+ parser(content_type, fields).parse response.body
35
+ end
36
+
37
+ private
38
+
39
+ def parser(content_type, fields)
40
+ case content_type
41
+ when :xml
42
+ fields = FIELD_TYPES.slice(*fields) unless fields.is_a? Hash
43
+ Serialization.xml_parser XML_RESPONSE_ROOT, fields
44
+ when :json
45
+ Serialization.json_parser fields
46
+ else
47
+ raise ResponseProcessorError 'Parser not implemented'
48
+ end
49
+ end
50
+
51
+ def service_parser(content_type)
52
+ case content_type
53
+ when :xml
54
+ @xml_service_parser || parser(:xml, SERVICE_FIELDS)
55
+ when :json
56
+ @json_service_parser || parser(:json, SERVICE_FIELDS)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'happymapper'
4
+
5
+ module IpApiService
6
+ module Serialization
7
+ module_function
8
+
9
+ class UnknownTypeError < StandardError; end
10
+
11
+ # подскажи, как обозвать
12
+ @type_tra_ta_ta = {
13
+ string: String,
14
+ integer: Integer,
15
+ float: Float,
16
+ time: Time,
17
+ date: Date,
18
+ datetime: DateTime,
19
+ boolean: HappyMapper::Boolean
20
+ }
21
+
22
+ class JsonParser
23
+ def initialize(fields)
24
+ fields = fields.keys if fields.is_a? Hash
25
+ @fields = fields
26
+ end
27
+
28
+ def parse(json_content)
29
+ raw = JSON.parse(json_content).transform_keys(&:to_sym).slice(*@fields)
30
+ Struct.new(*raw.keys).new(*raw.values_at(*raw.keys))
31
+ end
32
+ end
33
+
34
+ def json_parser(fields)
35
+ JsonParser.new fields
36
+ end
37
+
38
+ def xml_parser(root, fields)
39
+ parser = Class.new.include HappyMapper
40
+ parser.tag root
41
+ fields.each do |field, field_type|
42
+ begin
43
+ field_type = @type_tra_ta_ta.fetch field_type
44
+ rescue KeyError
45
+ raise UnknownTypeError "Unknown field type :#{field_type}"
46
+ end
47
+ parser.element field, field_type
48
+ end
49
+ parser
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IpApiService
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './ip_api_service/ip_api_adapter'
4
+ require 'resolv'
5
+
6
+ module IpApiService
7
+ extend self
8
+
9
+ AVAILABLE_LANGUAGES = %i[en de es pt-BR fr ja zh-CN ru].freeze
10
+
11
+ AVAILABLE_FORMATS = %i[ipMetaInfo json xml csv line php].freeze
12
+
13
+ DEFAULT_FIELDS = %i[country countryCode region regionName city zip lat lon timezone isp org as].freeze
14
+ private_constant :AVAILABLE_LANGUAGES, :AVAILABLE_FORMATS, :DEFAULT_FIELDS
15
+
16
+ @default_language = :en
17
+ @custom_default_fields = DEFAULT_FIELDS
18
+ @result_format = :ipMetaInfo
19
+
20
+ attr_reader :available_langviges, :available_formats, :default_language, :result_format
21
+
22
+ def available_fields
23
+ META_FIELDS.keys
24
+ end
25
+
26
+ def available_languages
27
+ AVAILABLE_LANGUAGES
28
+ end
29
+
30
+ def default_language=(value)
31
+ @default_language = AVAILABLE_LANGUAGES.include?(value) ? value : :en
32
+ end
33
+
34
+ def result_format=(value)
35
+ @result_format = AVAILABLE_FORMATS.include?(value) ? value : metaInfo
36
+ end
37
+
38
+ def field_description(field)
39
+ META_FIELDS[field][:description]
40
+ end
41
+
42
+ def default_fields=(value)
43
+ value &= META_FIELDS.keys
44
+ @custom_default_fields = value.empty? ? @default_fields : value
45
+ end
46
+
47
+ def default_fields
48
+ @custom_default_fields
49
+ end
50
+
51
+ def lookup(ip, fields: default_fields, result_format: @result_format, lang: @default_language)
52
+ raise ArgumentError, 'Unavailable result_format' unless AVAILABLE_FORMATS.include? result_format
53
+ raise ArgumentError, 'Unavailable language' unless AVAILABLE_LANGUAGES.include? lang
54
+
55
+ resolv = ip =~ Resolv::IPv4::Regex ? true : (ip =~ Resolv::IPv6::Regex)
56
+ raise ArgumentError, 'Invalid ip addess' unless resolv
57
+
58
+ fields = default_fields if fields.empty?
59
+ adapter.ip_meta_info ip, fields, result_format, lang
60
+ end
61
+
62
+ private
63
+
64
+ def adapter
65
+ @adapter ||= IpApiAdapter.new
66
+ end
67
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module WebService
6
+ class HttpUnknownMethodError < StandardError; end
7
+ class ConnectionError < StandardError; end
8
+
9
+ HTTP_COMMAND_METHODS = {
10
+ get: Net::HTTP::Get,
11
+ post: Net::HTTP::Post,
12
+ head: Net::HTTP::Head,
13
+ patch: Net::HTTP::Patch,
14
+ put: Net::HTTP::Put,
15
+ proppatch: Net::HTTP::Proppatch,
16
+ lock: Net::HTTP::Lock,
17
+ unlock: Net::HTTP::Unlock,
18
+ options: Net::HTTP::Options,
19
+ propfind: Net::HTTP::Propfind,
20
+ delete: Net::HTTP::Delete,
21
+ move: Net::HTTP::Move,
22
+ copy: Net::HTTP::Copy,
23
+ mkol: Net::HTTP::Mkcol,
24
+ trace: Net::HTTP::Trace
25
+ }.freeze
26
+ private_constant :HTTP_COMMAND_METHODS
27
+
28
+ class HttpCommand
29
+ def initialize(mapper)
30
+ @mapper = mapper
31
+ end
32
+
33
+ def execute(command, template, **params) # rubocop:disable Metrics/AbcSize
34
+ request_class = HTTP_COMMAND_METHODS[command]
35
+ raise HttpUnknownMethodError, "Unknown Http command: #{command}" unless request_class
36
+
37
+ headers = (params.delete :headers) || {}
38
+ body = params.delete :body
39
+ body = nil unless request_class::REQUEST_HAS_BODY
40
+ uri = @mapper.map_template template, **params
41
+ request = request_class.new uri, {}
42
+ headers.each { |header, value| request[header] = value }
43
+ begin
44
+ Net::HTTP.start(uri.host, uri.port) { |http| http.request request, body }
45
+ rescue StandardError
46
+ raise ConnectionError, 'Service unavailable'
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module WebService
6
+ class HttpServiceError < StandardError
7
+ attr_reader :status_code, :service_message
8
+
9
+ def initialize(status_code, service_message)
10
+ @status_code = status_code
11
+ @service_message = service_message
12
+ super 'Service error'
13
+ end
14
+ end
15
+
16
+ class HttpResponseProcessor
17
+ def process_response(response)
18
+ raise HttpServiceError.new(response.code, response.message) unless response.is_a? Net::HTTPSuccess
19
+
20
+ response
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable/template'
4
+
5
+ module WebService
6
+ module UriMapper
7
+ module_function
8
+
9
+ def map_template(template, **mapping)
10
+ Addressable::Template.new(template).expand mapping
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ module IpApiService
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ip_api_service
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - mike09
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-04-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - mike09@mail.ru
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rubocop.yml"
21
+ - Gemfile
22
+ - Gemfile.lock
23
+ - Makefile
24
+ - README.md
25
+ - Rakefile
26
+ - ip_api_service.gemspec
27
+ - lib/ip_api_service.rb
28
+ - lib/ip_api_service/ip_api_adapter.rb
29
+ - lib/ip_api_service/ip_api_response_processor.rb
30
+ - lib/ip_api_service/serialization.rb
31
+ - lib/ip_api_service/version.rb
32
+ - lib/web_service/http_command.rb
33
+ - lib/web_service/http_response_processor.rb
34
+ - lib/web_service/uri_mapper.rb
35
+ - sig/ip_api_service.rbs
36
+ homepage: https://github.com/mike090/ip_api_service
37
+ licenses: []
38
+ metadata:
39
+ homepage_uri: https://github.com/mike090/ip_api_service
40
+ source_code_uri: https://github.com/mike090/ip_api_service
41
+ changelog_uri: https://github.com/mike090/ip_api_service
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.6.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.3.7
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: API for ip-api.com
61
+ test_files: []