api_object 0.5.1 → 0.6.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.
- data/README.md +12 -2
- data/lib/api_object/version.rb +1 -1
- data/lib/api_object.rb +68 -13
- data/test/unit/api_object_test.rb +29 -7
- metadata +16 -16
    
        data/README.md
    CHANGED
    
    | @@ -129,7 +129,16 @@ The function takes parameters to define what fields from the location object are | |
| 129 129 | 
             
            data = Weather.get_results(:weather => '99.156.82.20')
         | 
| 130 130 | 
             
            ```
         | 
| 131 131 |  | 
| 132 | 
            -
            7)  | 
| 132 | 
            +
            7) Error handling
         | 
| 133 | 
            +
             | 
| 134 | 
            +
            In case data cannot be retrived (possible causes might be a wrong url or service downtime), the object returned is empty. Error messages could be checked using functions errors and has_errors?
         | 
| 135 | 
            +
             | 
| 136 | 
            +
            ```
         | 
| 137 | 
            +
            station.has_errors? 
         | 
| 138 | 
            +
            errors = station.errors
         | 
| 139 | 
            +
            ```
         | 
| 140 | 
            +
             | 
| 141 | 
            +
            8) Testing
         | 
| 133 142 |  | 
| 134 143 | 
             
            The gem has been tested on BART, Google Weather and NextBus APIs. 
         | 
| 135 144 |  | 
| @@ -143,9 +152,10 @@ API_KEY='<your key>' rake test | |
| 143 152 |  | 
| 144 153 | 
             
            There is no existing api key provided with this gem as per the Terms and Conditions of the ipinfodb service. 
         | 
| 145 154 |  | 
| 146 | 
            -
             | 
| 155 | 
            +
            9) Limitations
         | 
| 147 156 |  | 
| 148 157 | 
             
            * Api data must be presented either in XML or in JSON format. The distinction between XML and JSON is determinted automatically. 
         | 
| 158 | 
            +
            * Location by ip service uses a free database which is not always reliable. 
         | 
| 149 159 | 
             
            * When using this gem with external APIs, check Terms and Conditions of the API usage. 
         | 
| 150 160 | 
             
            * If something is not working, feel free to submit bugs and or/contribute. 
         | 
| 151 161 |  | 
    
        data/lib/api_object/version.rb
    CHANGED
    
    
    
        data/lib/api_object.rb
    CHANGED
    
    | @@ -4,17 +4,19 @@ require "active_support/all" | |
| 4 4 | 
             
            require "geo_ip"
         | 
| 5 5 |  | 
| 6 6 | 
             
            module ActiveApi
         | 
| 7 | 
            -
             | 
| 7 | 
            +
                    
         | 
| 8 8 | 
             
                module ClassMethods 
         | 
| 9 | 
            -
             | 
| 9 | 
            +
                      
         | 
| 10 10 | 
             
                  def self.extended(base)
         | 
| 11 11 | 
             
                    base.send(:extend, Query)     
         | 
| 12 12 | 
             
                  end
         | 
| 13 13 |  | 
| 14 14 | 
             
                  def initialize_from_api options = {}
         | 
| 15 | 
            +
                    
         | 
| 15 16 | 
             
                      class_attribute :url, :action, :key, :mode, :url_options, :data_tags, :object_name
         | 
| 16 17 | 
             
                      self.url, self.action, self.key, self.mode, self.url_options, self.data_tags, self.object_name = [options[:url], options[:action], options[:key], options[:mode], (options[:url_options] || {}), ([*options[:data_tags]] || []), (options[:object_name] || self.to_s.downcase.gsub(/^(.+::)(.+)$/, '\2'))]
         | 
| 17 18 | 
             
                      instance_eval do
         | 
| 19 | 
            +
                        
         | 
| 18 20 |  | 
| 19 21 | 
             
                         def get_results_by_ip ip, arguments = {}
         | 
| 20 22 | 
             
                            self.api_key = arguments.delete(:key) if arguments[:key]
         | 
| @@ -22,8 +24,8 @@ module ActiveApi | |
| 22 24 | 
             
                            raise unless location[:status_code] == "OK"
         | 
| 23 25 | 
             
                            get_results [*arguments.keys].inject({}) { |opts, a| opts.merge(a.to_sym => location[arguments[a.to_sym]]) }
         | 
| 24 26 | 
             
                            rescue
         | 
| 25 | 
            -
                              puts " | 
| 26 | 
            -
                              return  | 
| 27 | 
            +
                              puts "ERROR: Cannot get results or location by ip. Verify that you have a valid key for the ipinfodb.com service"
         | 
| 28 | 
            +
                              return ApiObjectError.new(:class => self, :errors => invalid_loc_msg)
         | 
| 27 29 | 
             
                         end
         | 
| 28 30 |  | 
| 29 31 | 
             
                         def get_results options = {}
         | 
| @@ -32,8 +34,8 @@ module ActiveApi | |
| 32 34 | 
             
                           result = query_api(self.url, self.action, self.mode, self.url_options.merge(options))
         | 
| 33 35 | 
             
                           process_result result
         | 
| 34 36 | 
             
                           rescue
         | 
| 35 | 
            -
                             puts " | 
| 36 | 
            -
                             return  | 
| 37 | 
            +
                             puts "ERROR: The request returned no valid data. #{error_invalid_url result[:url]}"
         | 
| 38 | 
            +
                             return ApiObjectError.new(:class => self, :errors => invalid_url_msg)
         | 
| 37 39 | 
             
                         end 
         | 
| 38 40 |  | 
| 39 41 | 
             
                         def api_key=(key)
         | 
| @@ -44,9 +46,21 @@ module ActiveApi | |
| 44 46 | 
             
                            GeoIp.api_key
         | 
| 45 47 | 
             
                         end
         | 
| 46 48 |  | 
| 47 | 
            -
                         def  | 
| 49 | 
            +
                         def error_invalid_url url
         | 
| 48 50 | 
             
                           "The request url is #{url}, please, check if it's invalid of there is no connectivity." unless url.nil?
         | 
| 49 51 | 
             
                         end
         | 
| 52 | 
            +
                         
         | 
| 53 | 
            +
                         def invalid_url_msg
         | 
| 54 | 
            +
                           "Cannot get results from the url"
         | 
| 55 | 
            +
                         end
         | 
| 56 | 
            +
                         
         | 
| 57 | 
            +
                         def invalid_loc_msg
         | 
| 58 | 
            +
                           "Cannot obtain results by ip, check the location"
         | 
| 59 | 
            +
                         end
         | 
| 60 | 
            +
                         
         | 
| 61 | 
            +
                         def invalid_data_msg
         | 
| 62 | 
            +
                           "Cannot initialize the object"
         | 
| 63 | 
            +
                         end
         | 
| 50 64 |  | 
| 51 65 | 
             
                         private
         | 
| 52 66 | 
             
                         def process_result result
         | 
| @@ -74,9 +88,10 @@ module ActiveApi | |
| 74 88 | 
             
               end
         | 
| 75 89 |  | 
| 76 90 | 
             
               module InstanceMethods
         | 
| 77 | 
            -
             | 
| 91 | 
            +
                            
         | 
| 78 92 | 
             
                  def initialize(*args)
         | 
| 79 93 | 
             
                      url = args.first.delete(:url)
         | 
| 94 | 
            +
                      errors = args.first.delete(:errors)
         | 
| 80 95 | 
             
                      tags = respond_to?('data_tags') ? self.data_tags : args.last[:tags]
         | 
| 81 96 | 
             
                      args.first.each do |k, v| 
         | 
| 82 97 | 
             
                         k = k.gsub(/@/, '')
         | 
| @@ -87,11 +102,18 @@ module ActiveApi | |
| 87 102 | 
             
                            instance_variable_set("@#{k.to_s}", result)
         | 
| 88 103 | 
             
                         end
         | 
| 89 104 | 
             
                      end if args.first.is_a?(Hash)
         | 
| 90 | 
            -
                       | 
| 105 | 
            +
                      if self.empty?
         | 
| 106 | 
            +
                        puts "ERROR: data passed for #{self.class} initialization was invalid. #{self.class.error_invalid_url url}" 
         | 
| 107 | 
            +
                        @errors = errors || self.class.invalid_data_msg
         | 
| 108 | 
            +
                      end
         | 
| 91 109 | 
             
                  end
         | 
| 92 110 |  | 
| 93 111 | 
             
                  def empty?
         | 
| 94 | 
            -
                      self.instance_variables.empty?
         | 
| 112 | 
            +
                      (self.instance_variables - [:@errors]).empty?
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                  
         | 
| 115 | 
            +
                  def has_errors?
         | 
| 116 | 
            +
                    self.instance_variables.include?(:@errors)
         | 
| 95 117 | 
             
                  end
         | 
| 96 118 |  | 
| 97 119 |  | 
| @@ -142,8 +164,41 @@ module ActiveApi | |
| 142 164 | 
             
                   class_attribute :columns, :assoc 
         | 
| 143 165 | 
             
                   self.columns, self.assoc = [{}, {}]
         | 
| 144 166 |  | 
| 145 | 
            -
             | 
| 167 | 
            +
                   attr_reader :errors
         | 
| 168 | 
            +
                   
         | 
| 169 | 
            +
                end 
         | 
| 146 170 |  | 
| 147 | 
            -
             | 
| 171 | 
            +
                class ApiObjectError < Hash
         | 
| 172 | 
            +
                    attr_reader :errors, :klass
         | 
| 173 | 
            +
                    
         | 
| 174 | 
            +
                    def initialize *args
         | 
| 175 | 
            +
                      options = args.extract_options!
         | 
| 176 | 
            +
                      @errors = options.delete(:errors)
         | 
| 177 | 
            +
                      @klass = options.delete(:class)
         | 
| 178 | 
            +
                    end
         | 
| 179 | 
            +
                            
         | 
| 180 | 
            +
                    def keys
         | 
| 181 | 
            +
                      [:errors]
         | 
| 182 | 
            +
                    end
         | 
| 183 | 
            +
                    
         | 
| 184 | 
            +
                    def [](key)
         | 
| 185 | 
            +
                      eval("self.#{key.to_s}") if keys.include?(key)
         | 
| 186 | 
            +
                    end
         | 
| 187 | 
            +
                    
         | 
| 188 | 
            +
                    def map &block
         | 
| 189 | 
            +
                      klass.new({:errors => @errors})
         | 
| 190 | 
            +
                    end
         | 
| 191 | 
            +
                    
         | 
| 192 | 
            +
                    alias_method :collect, :map
         | 
| 193 | 
            +
                    
         | 
| 194 | 
            +
                    def inspect
         | 
| 195 | 
            +
                      "ApiObjectError: #{@errors}"
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
                    
         | 
| 198 | 
            +
                    def to_s
         | 
| 199 | 
            +
                      inspect
         | 
| 200 | 
            +
                    end
         | 
| 201 | 
            +
                end 
         | 
| 202 | 
            +
                   
         | 
| 148 203 | 
             
              end
         | 
| 149 | 
            -
             | 
| 204 | 
            +
                      
         | 
| @@ -8,11 +8,15 @@ class ApiObjectTest < MiniTest::Unit::TestCase | |
| 8 8 | 
             
              include ActiveApi
         | 
| 9 9 | 
             
              include TestObjects
         | 
| 10 10 |  | 
| 11 | 
            +
              IP = '99.156.82.20'
         | 
| 12 | 
            +
              GeoIp.api_key = nil 
         | 
| 13 | 
            +
              
         | 
| 11 14 | 
             
              @@data_directory = File.expand_path('../../data', __FILE__)
         | 
| 12 15 | 
             
              @@estimate_directory = @@data_directory + "/estimate"
         | 
| 13 16 | 
             
              @@weather_directory = @@data_directory + "/weather"
         | 
| 14 17 | 
             
              @@bus_directory = @@data_directory + "/bus"
         | 
| 15 18 | 
             
              @@key_directory = @@data_directory +"/keys"
         | 
| 19 | 
            +
              
         | 
| 16 20 |  | 
| 17 21 | 
             
              @@glen_estimate = Station.load_from_xml(File.read(@@estimate_directory + '/glen.xml'))    
         | 
| 18 22 | 
             
              @@sixteenth_estimate = Station.load_from_xml(File.read(@@estimate_directory + '/sixteenth.xml'))
         | 
| @@ -21,8 +25,6 @@ class ApiObjectTest < MiniTest::Unit::TestCase | |
| 21 25 | 
             
              @@weather_mv = Weather.load_from_xml(File.read(@@weather_directory + '/mountain_view.xml'))
         | 
| 22 26 | 
             
              @@weather_au = Weather.load_from_xml(File.read(@@weather_directory + '/austin.xml'))
         | 
| 23 27 | 
             
              @@ip_key = File.read(@@key_directory + "/ipinfodb_key.txt") rescue ENV['API_KEY']
         | 
| 24 | 
            -
              IP = '99.156.82.20'
         | 
| 25 | 
            -
              GeoIp.api_key = nil
         | 
| 26 28 |  | 
| 27 29 | 
             
              @@muni_routes = Route.load_from_xml(File.read(@@bus_directory + "/muni_routes.xml"))
         | 
| 28 30 | 
             
              @@muni_F = Route.load_from_xml(File.read(@@bus_directory + "/muni_F.xml"))
         | 
| @@ -64,6 +66,7 @@ class ApiObjectTest < MiniTest::Unit::TestCase | |
| 64 66 | 
             
                unless @@ip_key.nil?
         | 
| 65 67 | 
             
                  weather_au = Weather.new(Weather.get_results_by_ip(IP, :key => @@ip_key, :weather => :zip_code))
         | 
| 66 68 | 
             
                  assert_equal(weather_au, @@weather_au)
         | 
| 69 | 
            +
                  refute_has_errors(weather_au)
         | 
| 67 70 | 
             
                end
         | 
| 68 71 | 
             
              end
         | 
| 69 72 |  | 
| @@ -71,33 +74,44 @@ class ApiObjectTest < MiniTest::Unit::TestCase | |
| 71 74 | 
             
                GeoIp.api_key = @@ip_key
         | 
| 72 75 | 
             
                weather_au = Weather.new(Weather.get_results_by_ip(IP, :weather => :zip_code))
         | 
| 73 76 | 
             
                assert_equal(weather_au, @@weather_au)
         | 
| 77 | 
            +
                refute_has_errors(weather_au)
         | 
| 74 78 | 
             
              end
         | 
| 75 | 
            -
             | 
| 79 | 
            +
               
         | 
| 76 80 | 
             
              def test_should_not_get_correct_weather_with_no_key
         | 
| 77 81 | 
             
                GeoIp.api_key = nil
         | 
| 78 82 | 
             
                weather_au = Weather.new(Weather.get_results_by_ip(IP, :weather => :zip_code))
         | 
| 79 83 | 
             
                assert_empty(weather_au)
         | 
| 84 | 
            +
                assert_has_errors(weather_au)
         | 
| 80 85 | 
             
              end
         | 
| 81 86 |  | 
| 82 87 | 
             
              def test_should_get_correct_bus_routes
         | 
| 83 88 | 
             
                routes = Route.get_results(:a => 'sf-muni', :command => 'routeList').map {|r| Route.new(r)}
         | 
| 84 89 | 
             
                assert_equal_route_list(routes, @@muni_routes)
         | 
| 90 | 
            +
                routes.map {|r| refute_has_errors(r)}
         | 
| 85 91 | 
             
              end
         | 
| 86 | 
            -
             | 
| 92 | 
            +
             | 
| 87 93 | 
             
              def test_should_get_correct_route_information
         | 
| 88 94 | 
             
                route_F = Route.new(Route.get_results(:a => 'sf-muni', :command => 'routeConfig', :r => 'F', :mode => 'terse'))
         | 
| 89 95 | 
             
                assert_equal(route_F, @@muni_F)
         | 
| 96 | 
            +
                refute_has_errors(route_F)
         | 
| 90 97 | 
             
              end
         | 
| 91 | 
            -
             | 
| 98 | 
            +
             | 
| 92 99 | 
             
              def test_should_handle_invalid_url
         | 
| 93 | 
            -
                estimate = Station.new(Station.get_results(:orig => 'YES'))
         | 
| 100 | 
            +
                estimate = Station.new(Station.get_results(:orig => 'YES'))  
         | 
| 94 101 | 
             
                assert(estimate.empty?, "Estimate should be an empty object")
         | 
| 102 | 
            +
                assert_has_errors(estimate)
         | 
| 103 | 
            +
                assert(estimate.errors, Station.invalid_url_msg)
         | 
| 95 104 | 
             
                weather = Weather.new(Weather.get_results(:weather => "Here"))
         | 
| 96 105 | 
             
                assert(weather.empty?, "Weather should be an empty object")
         | 
| 97 | 
            -
                 | 
| 106 | 
            +
                assert_has_errors(weather)
         | 
| 107 | 
            +
                assert(weather.errors, Weather.invalid_data_msg)
         | 
| 108 | 
            +
                routes = Route.get_results(:a => 'sffg').collect {|r| Route.new(r)}
         | 
| 98 109 | 
             
                assert(routes.empty?, "Routes should be an empty object")
         | 
| 110 | 
            +
                assert_has_errors(routes)
         | 
| 111 | 
            +
                assert(routes.errors, Route.invalid_url_msg)
         | 
| 99 112 | 
             
              end
         | 
| 100 113 |  | 
| 114 | 
            +
              
         | 
| 101 115 | 
             
            private  
         | 
| 102 116 |  | 
| 103 117 | 
             
              #ensure that the estimates of the first station include the sample
         | 
| @@ -119,6 +133,14 @@ private | |
| 119 133 | 
             
                return assert(route_list.include_all?(sample_route_list), "Route list should include all the sample routes")
         | 
| 120 134 | 
             
              end
         | 
| 121 135 |  | 
| 136 | 
            +
              def assert_has_errors obj
         | 
| 137 | 
            +
                return assert(obj.has_errors?, "Object should have errors")
         | 
| 138 | 
            +
              end
         | 
| 139 | 
            +
              
         | 
| 140 | 
            +
              def refute_has_errors obj
         | 
| 141 | 
            +
                return refute(obj.has_errors?, "Object shouldn't have errors")
         | 
| 142 | 
            +
              end
         | 
| 143 | 
            +
              
         | 
| 122 144 |  | 
| 123 145 | 
             
              def business_time? now
         | 
| 124 146 | 
             
                unless now.to_date.sunday? 
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: api_object
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.6.0
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -9,11 +9,11 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date: 2012-06- | 
| 12 | 
            +
            date: 2012-06-16 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: activemodel
         | 
| 16 | 
            -
              requirement: & | 
| 16 | 
            +
              requirement: &76922910 !ruby/object:Gem::Requirement
         | 
| 17 17 | 
             
                none: false
         | 
| 18 18 | 
             
                requirements:
         | 
| 19 19 | 
             
                - - ! '>='
         | 
| @@ -21,10 +21,10 @@ dependencies: | |
| 21 21 | 
             
                    version: '0'
         | 
| 22 22 | 
             
              type: :development
         | 
| 23 23 | 
             
              prerelease: false
         | 
| 24 | 
            -
              version_requirements: * | 
| 24 | 
            +
              version_requirements: *76922910
         | 
| 25 25 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 26 26 | 
             
              name: rspec
         | 
| 27 | 
            -
              requirement: & | 
| 27 | 
            +
              requirement: &76922660 !ruby/object:Gem::Requirement
         | 
| 28 28 | 
             
                none: false
         | 
| 29 29 | 
             
                requirements:
         | 
| 30 30 | 
             
                - - ! '>='
         | 
| @@ -32,10 +32,10 @@ dependencies: | |
| 32 32 | 
             
                    version: '0'
         | 
| 33 33 | 
             
              type: :development
         | 
| 34 34 | 
             
              prerelease: false
         | 
| 35 | 
            -
              version_requirements: * | 
| 35 | 
            +
              version_requirements: *76922660
         | 
| 36 36 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 37 37 | 
             
              name: minitest
         | 
| 38 | 
            -
              requirement: & | 
| 38 | 
            +
              requirement: &77001800 !ruby/object:Gem::Requirement
         | 
| 39 39 | 
             
                none: false
         | 
| 40 40 | 
             
                requirements:
         | 
| 41 41 | 
             
                - - ! '>='
         | 
| @@ -43,10 +43,10 @@ dependencies: | |
| 43 43 | 
             
                    version: '0'
         | 
| 44 44 | 
             
              type: :development
         | 
| 45 45 | 
             
              prerelease: false
         | 
| 46 | 
            -
              version_requirements: * | 
| 46 | 
            +
              version_requirements: *77001800
         | 
| 47 47 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 48 48 | 
             
              name: activesupport
         | 
| 49 | 
            -
              requirement: & | 
| 49 | 
            +
              requirement: &77001550 !ruby/object:Gem::Requirement
         | 
| 50 50 | 
             
                none: false
         | 
| 51 51 | 
             
                requirements:
         | 
| 52 52 | 
             
                - - ! '>='
         | 
| @@ -54,10 +54,10 @@ dependencies: | |
| 54 54 | 
             
                    version: '0'
         | 
| 55 55 | 
             
              type: :runtime
         | 
| 56 56 | 
             
              prerelease: false
         | 
| 57 | 
            -
              version_requirements: * | 
| 57 | 
            +
              version_requirements: *77001550
         | 
| 58 58 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 59 59 | 
             
              name: nori
         | 
| 60 | 
            -
              requirement: & | 
| 60 | 
            +
              requirement: &77001240 !ruby/object:Gem::Requirement
         | 
| 61 61 | 
             
                none: false
         | 
| 62 62 | 
             
                requirements:
         | 
| 63 63 | 
             
                - - ! '>='
         | 
| @@ -65,10 +65,10 @@ dependencies: | |
| 65 65 | 
             
                    version: '1.1'
         | 
| 66 66 | 
             
              type: :runtime
         | 
| 67 67 | 
             
              prerelease: false
         | 
| 68 | 
            -
              version_requirements: * | 
| 68 | 
            +
              version_requirements: *77001240
         | 
| 69 69 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 70 70 | 
             
              name: rest-client
         | 
| 71 | 
            -
              requirement: & | 
| 71 | 
            +
              requirement: &77000930 !ruby/object:Gem::Requirement
         | 
| 72 72 | 
             
                none: false
         | 
| 73 73 | 
             
                requirements:
         | 
| 74 74 | 
             
                - - ! '>='
         | 
| @@ -76,10 +76,10 @@ dependencies: | |
| 76 76 | 
             
                    version: '1.6'
         | 
| 77 77 | 
             
              type: :runtime
         | 
| 78 78 | 
             
              prerelease: false
         | 
| 79 | 
            -
              version_requirements: * | 
| 79 | 
            +
              version_requirements: *77000930
         | 
| 80 80 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 81 81 | 
             
              name: geo_ip
         | 
| 82 | 
            -
              requirement: & | 
| 82 | 
            +
              requirement: &77000700 !ruby/object:Gem::Requirement
         | 
| 83 83 | 
             
                none: false
         | 
| 84 84 | 
             
                requirements:
         | 
| 85 85 | 
             
                - - ! '>='
         | 
| @@ -87,7 +87,7 @@ dependencies: | |
| 87 87 | 
             
                    version: '0'
         | 
| 88 88 | 
             
              type: :runtime
         | 
| 89 89 | 
             
              prerelease: false
         | 
| 90 | 
            -
              version_requirements: * | 
| 90 | 
            +
              version_requirements: *77000700
         | 
| 91 91 | 
             
            description: An interface to load objects from external APIs provided in XML and JSON
         | 
| 92 92 | 
             
              formats
         | 
| 93 93 | 
             
            email:
         |