ip_api_service 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []