my_john_deere_api 2.3.3 → 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +236 -3
  3. data/lib/my_john_deere_api.rb +1 -0
  4. data/lib/my_john_deere_api/client.rb +13 -3
  5. data/lib/my_john_deere_api/net_http_retry.rb +2 -0
  6. data/lib/my_john_deere_api/net_http_retry/decorator.rb +53 -0
  7. data/lib/my_john_deere_api/net_http_retry/max_retries_exceeded_error.rb +20 -0
  8. data/lib/my_john_deere_api/version.rb +1 -1
  9. data/test/lib/my_john_deere_api/client_test.rb +20 -3
  10. data/test/lib/my_john_deere_api/errors/max_retries_exceeded_error_test.rb +13 -0
  11. data/test/lib/my_john_deere_api/net_http_retry/decorator_test.rb +163 -0
  12. data/test/support/helper.rb +1 -0
  13. data/test/support/vcr/accessor/delete_failed.yml +327 -0
  14. data/test/support/vcr/accessor/delete_max_failed.yml +615 -0
  15. data/test/support/vcr/accessor/delete_retry.yml +191 -0
  16. data/test/support/vcr/accessor/delete_retry_too_soon.yml +191 -0
  17. data/test/support/vcr/accessor/get_failed.yml +390 -0
  18. data/test/support/vcr/accessor/get_max_failed.yml +734 -0
  19. data/test/support/vcr/accessor/get_retry.yml +226 -0
  20. data/test/support/vcr/accessor/get_retry_too_soon.yml +226 -0
  21. data/test/support/vcr/accessor/post_failed.yml +417 -0
  22. data/test/support/vcr/accessor/post_max_failed.yml +785 -0
  23. data/test/support/vcr/accessor/post_retry.yml +241 -0
  24. data/test/support/vcr/accessor/post_retry_too_soon.yml +241 -0
  25. data/test/support/vcr/accessor/put_failed.yml +372 -0
  26. data/test/support/vcr/accessor/put_max_failed.yml +700 -0
  27. data/test/support/vcr/accessor/put_retry.yml +216 -0
  28. data/test/support/vcr/accessor/put_retry_too_soon.yml +216 -0
  29. metadata +24 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e82399b62eca7ecbf5c1e1f9d7f0de2391c909bed4426ccafbc19a3b555a2df2
4
- data.tar.gz: e38342e0db2b4adc5cc6a9a6bba54b91c7aeb2e56e819a25696fc3c0cde75447
3
+ metadata.gz: 75b86b9b093f4aa0cc586ce56fb961654b1b91577947acf48392d8d2294c85b9
4
+ data.tar.gz: b5879b20c85891180747ba42657ffa399da981596820c813a7049d680240dd39
5
5
  SHA512:
6
- metadata.gz: 2e80cfb7dbd6f7fb209403d5d92405e1beacf5282612747f449d245dffea7b182e2dc913a57acbe478a9f986e8b6b421add72e525c9d34def8ffbfe22967dd15
7
- data.tar.gz: 743a9373b0646e76ace34001ae95937555c1aa00eea2c9bbcc4b6d6896f4784d67d1ecd07b4e7116182de60066c01d5ab4468c595f2f68f0a9ad401baf727a69
6
+ metadata.gz: 6a8c729311baa853f8ae1039573f673cb01cc084d0fc9b662ce592dd1b907a85b153f04d07ebd6bf346558efb708242f5c13f3464f3c4c5b03a4c21a2479c2d9
7
+ data.tar.gz: cf5dd0b1e590baeb06ead7ffeffb4a7501de17b8b56596a297797186f1bc85aa33b196f58f19162f6027e40a27b023f9ad18ad3aa25e07b61fcd383612c0bbaa
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CircleCI](https://circleci.com/gh/Intellifarm/my_john_deere_api.svg?style=svg)](https://circleci.com/gh/Intellifarm/my_john_deere_api)
4
4
 
5
5
  This client allows you to connect the [MyJohnDeere API](https://developer.deere.com/#!documentation)
6
- without having to code your own oauth process, API requests, and pagination.
6
+ without having to code your own oAuth process, API requests, and pagination.
7
7
 
8
8
  * Works with Rails, but does not require it
9
9
  * Supports both sandbox and live mode
@@ -23,6 +23,8 @@ without having to code your own oauth process, API requests, and pagination.
23
23
  * [Contribution Definitions](#contribution-definitions)
24
24
  * [Organizations](#organizations)
25
25
  * [Assets](#assets)
26
+ * [Asset Locations](#asset-locations)
27
+ * [Fields](#fields)
26
28
  * [Direct API Requests](#direct-api-requests)
27
29
  * [GET](#get)
28
30
  * [POST](#post)
@@ -345,7 +347,7 @@ organization.links
345
347
  # => {
346
348
  # 'self' => 'https://sandboxapi.deere.com/platform/organizations/1234',
347
349
  # 'machines' => 'https://sandboxapi.deere.com/platform/organizations/1234/machines',
348
- # 'wdtCapableMachines' => 'ttps://sandboxapi.deere.com/platform/organizations/1234/machines?capability=wdt'
350
+ # 'wdtCapableMachines' => 'https://sandboxapi.deere.com/platform/organizations/1234/machines?capability=wdt'
349
351
  # }
350
352
 
351
353
  organization.assets
@@ -435,6 +437,237 @@ asset.save
435
437
  ```
436
438
 
437
439
 
440
+ ### [Asset Locations](https://developer.deere.com/#!documentation&doc=.%2Fmyjohndeere%2Fassets.htm)
441
+
442
+ Handles an asset's locations. Asset Location collections support the following methods:
443
+
444
+ * create(attributes)
445
+ * all
446
+ * count
447
+ * first
448
+
449
+ An individual location supports the following methods:
450
+
451
+ * timestamp
452
+ * geometry
453
+ * measurement\_data
454
+
455
+ ```ruby
456
+ asset = organizations.assets.first
457
+ # => the first asset returned by the organization
458
+
459
+ asset.locations
460
+ # => collection of locations belonging to this asset
461
+
462
+ location = asset.locations.first
463
+ # => the first location returned by the asset. Note that locations do not have their own id's
464
+ # in the JD platform, and therefore cannot be requested individually via a "find" method.
465
+
466
+ location.timestamp
467
+ # => "2019-11-11T23:00:00.000Z"
468
+ # John Deere includes 3 decimal places in the format, but does not actually
469
+ # store fractions of a second, so it will always end in ".000". This is
470
+ # important, because timestamps must be unique.
471
+
472
+ location.geometry
473
+ # => a GeoJSON formatted hash, for example:
474
+ # {
475
+ # "type"=>"Feature",
476
+ # "geometry"=>{
477
+ # "geometries"=>[
478
+ # {
479
+ # "coordinates"=>[-95.123456, 40.123456],
480
+ # "type"=>"Point"
481
+ # }
482
+ # ],
483
+ # "type"=>"GeometryCollection"
484
+ # }
485
+ # }
486
+
487
+ location.measurement_data
488
+ # => the status details of this location, for example:
489
+ # [
490
+ # {
491
+ # "@type"=>"BasicMeasurement",
492
+ # "name"=>"[Soil Temperature](http://example.com/current_temperature)",
493
+ # "value"=>"21.0",
494
+ # "unit"=>"°C"
495
+ # }
496
+ # ]
497
+ ```
498
+
499
+ The `create` method creates the location in the John Deere platform, and returns the newly created
500
+ object from John Deere. However, there will be no new information since there is no unique ID
501
+ generated. The timestamp submitted (which defaults to "now") will be rounded
502
+ to the nearest second.
503
+
504
+ ```ruby
505
+ locaton = asset.locatons.create(
506
+ # You can pass fractional seconds, but they will be truncated by JD.
507
+ timestamp: "2019-11-11T23:00:00.123Z",
508
+
509
+ # JD requires more complicated JSON geometry, but this client will convert a simple
510
+ # set of lat/long coordinates into the larger format automatically.
511
+ geometry: [-95.123456, 40.123456],
512
+
513
+ # This is a list of "measurements"
514
+ measurement_data: [
515
+ {
516
+ name: 'Temperature',
517
+ value: '68.0',
518
+ unit: 'F'
519
+ }
520
+ ]
521
+ )
522
+
523
+ location.timestamp
524
+ # => "2019-11-11T23:00:00.000Z"
525
+ # Note that the timestamp's fractional second is truncated by John Deere, though they
526
+ # still return the record with three digits of precision.
527
+
528
+ location.geometry
529
+ # => a GeoJSON formatted hash in its larger format
530
+ # {
531
+ # "type"=>"Feature",
532
+ # "geometry"=>{
533
+ # "geometries"=>[
534
+ # {
535
+ # "coordinates"=>[-95.123456, 40.123456],
536
+ # "type"=>"Point"
537
+ # }
538
+ # ],
539
+ # "type"=>"GeometryCollection"
540
+ # }
541
+ # }
542
+
543
+ location.measurement_data
544
+ # [
545
+ # {
546
+ # "@type"=>"BasicMeasurement",
547
+ # "name"=>"Temperature",
548
+ # "value"=>"68.0",
549
+ # "unit"=>"F"
550
+ # }
551
+ # ]
552
+
553
+ ```
554
+
555
+ There is no updating or deleting of a location. The newest location record always acts as the status
556
+ for the given asset, and is what appears on the map view.
557
+
558
+ Note that locations are called "Asset Locations" in John Deere, but we call the association "locations", as in
559
+ `asset.locations`, for brevity.
560
+
561
+
562
+ ### [Fields](https://developer.deere.com/#!documentation&doc=myjohndeere%2FfieldsADS.htm)
563
+
564
+ Handles an organization's fields. Field collections support the following methods:
565
+
566
+ * all
567
+ * count
568
+ * first
569
+ * find(field\_id)
570
+
571
+ An individual field supports the following methods and associations:
572
+
573
+ * id
574
+ * name
575
+ * archived?
576
+ * links
577
+ * flags (collection of this field's flags)
578
+
579
+ The `count` method only requires loading the first page of results, so it's a relatively cheap call. On the other hand,
580
+ `all` forces the entire collection to be loaded from John Deere's API, so use with caution. Fields can be
581
+ created via the API, but there is no `create` method on this collection yet.
582
+
583
+ ```ruby
584
+ organization.fields
585
+ # => collection of fields under this organization
586
+
587
+ organization.fields.count
588
+ # => 15
589
+
590
+ organization.fields.first
591
+ # => an individual field object
592
+
593
+ field = organization.fields.find(1234)
594
+ # => an individual field object, fetched by ID
595
+
596
+ field.name
597
+ # => 'Smith Field'
598
+
599
+ field.archived?
600
+ # => false
601
+
602
+ field.links
603
+ # => a hash of API urls related to this field
604
+
605
+ field.flags
606
+ # => collection of flags belonging to this field
607
+ ```
608
+
609
+
610
+ ### [Flags](https://developer.deere.com/#!documentation&doc=.%2Fmyjohndeere%2Fflags.htm)
611
+
612
+ Handles a field's flags. Flag collections support the following methods. Note, John Deere does not provide an endpoint to retrieve a specific flag by id:
613
+
614
+ * all
615
+ * count
616
+ * first
617
+
618
+ An individual flag supports the following methods and associations:
619
+
620
+ * id
621
+ * notes
622
+ * geometry
623
+ * archived?
624
+ * proximity\_alert\_enabled?
625
+ * links
626
+
627
+ The `count` method only requires loading the first page of results, so it's a relatively cheap call. On the other hand,
628
+ `all` forces the entire collection to be loaded from John Deere's API, so use with caution. Flags can be
629
+ created via the API, but there is no `create` method on this collection yet.
630
+
631
+ ```ruby
632
+ field.flags
633
+ # => collection of flags under this field
634
+
635
+ field.flags.count
636
+ # => 15
637
+
638
+ flag = field.flags.first
639
+ # => an individual flag object
640
+
641
+ flag.notes
642
+ # => 'A big rock on the left after entering the field'
643
+
644
+ flag.geometry
645
+ # => a GeoJSON formatted hash, for example:
646
+ # {
647
+ # "type"=>"Feature",
648
+ # "geometry"=>{
649
+ # "geometries"=>[
650
+ # {
651
+ # "coordinates"=>[-95.123456, 40.123456],
652
+ # "type"=>"Point"
653
+ # }
654
+ # ],
655
+ # "type"=>"GeometryCollection"
656
+ # }
657
+ # }
658
+
659
+
660
+ field.archived?
661
+ # => false
662
+
663
+ field.proximity_alert_enabled?
664
+ # => true
665
+
666
+ field.links
667
+ # => a hash of API urls related to this flag
668
+ ```
669
+
670
+
438
671
  ## Direct API Requests
439
672
 
440
673
  While the goal of the client is to eliminate the need to make/interpret calls to the John Deere API, it's important
@@ -544,7 +777,7 @@ Custom errors help clearly identify problems when using the client:
544
777
  * **InvalidRecordError** is raised when bad input has been given, in an attempt to create or update
545
778
  a record on the John Deere platform.
546
779
  * **MissingContributionDefinitionIdError** is raised when the optional contribution\_definition\_id
547
- has not been set in the client, but an operation has been attempted that requires it - like
780
+ has not been set in the client, but an operation has been attempted that requires it - like
548
781
  creating an asset in the John Deere platform.
549
782
  * **TypeMismatchError** is raised when a model is instantiated, typically when a record is received
550
783
  from John Deere and is being converted into a Ruby object. Model instantiation is normally handled
@@ -13,4 +13,5 @@ module MyJohnDeereApi
13
13
  autoload :Validators, 'my_john_deere_api/validators'
14
14
 
15
15
  require 'my_john_deere_api/errors'
16
+ require 'my_john_deere_api/net_http_retry'
16
17
  end
@@ -4,10 +4,11 @@ module MyJohnDeereApi
4
4
  include Helpers::CaseConversion
5
5
 
6
6
  attr_accessor :contribution_definition_id
7
- attr_reader :api_key, :api_secret, :access_token, :access_secret
7
+ attr_reader :api_key, :api_secret, :access_token, :access_secret, :http_retry_options
8
8
 
9
9
  DEFAULTS = {
10
- environment: :live
10
+ environment: :live,
11
+ http_retry: {}
11
12
  }
12
13
 
13
14
  ##
@@ -37,6 +38,7 @@ module MyJohnDeereApi
37
38
 
38
39
  self.environment = options[:environment]
39
40
  @contribution_definition_id = options[:contribution_definition_id]
41
+ @http_retry_options = options[:http_retry]
40
42
  end
41
43
 
42
44
  ##
@@ -45,7 +47,15 @@ module MyJohnDeereApi
45
47
 
46
48
  def accessor
47
49
  return @accessor if defined?(@accessor)
48
- @accessor = OAuth::AccessToken.new(consumer.user_get, access_token, access_secret)
50
+
51
+ @accessor = NetHttpRetry::Decorator.new(
52
+ OAuth::AccessToken.new(
53
+ consumer.user_get,
54
+ access_token,
55
+ access_secret
56
+ ),
57
+ http_retry_options
58
+ )
49
59
  end
50
60
 
51
61
  ##
@@ -0,0 +1,2 @@
1
+ require 'my_john_deere_api/net_http_retry/decorator'
2
+ require 'my_john_deere_api/net_http_retry/max_retries_exceeded_error'
@@ -0,0 +1,53 @@
1
+ module MyJohnDeereApi
2
+ module NetHttpRetry
3
+ class Decorator
4
+ attr_reader :object, :request_methods, :retry_delay_exponent, :max_retries, :response_codes
5
+
6
+ DEFAULTS = {
7
+ request_methods: [:get, :post, :put, :delete],
8
+ retry_delay_exponent: 2,
9
+ max_retries: 12,
10
+ response_codes: ['429', '503']
11
+ }
12
+
13
+ def initialize(object, options={})
14
+ @object = object
15
+
16
+ [:request_methods, :retry_delay_exponent, :max_retries].each do |option|
17
+ instance_variable_set(:"@#{option}", options[option] || DEFAULTS[option])
18
+ end
19
+
20
+ @response_codes = (options[:response_codes] || DEFAULTS[:response_codes]).map(&:to_s)
21
+ end
22
+
23
+ def request(method_name, *args)
24
+ retries = 0
25
+ result = object.send(method_name, *args)
26
+
27
+ while response_codes.include?(result.code)
28
+ if retries >= max_retries
29
+ raise MaxRetriesExceededError.new(method_name, "#{result.code} #{result.message}")
30
+ end
31
+
32
+ delay = [result['retry-after'].to_i, retry_delay_exponent ** retries].max
33
+ sleep(delay)
34
+
35
+ result = object.send(method_name, *args)
36
+ retries += 1
37
+ end
38
+
39
+ result
40
+ end
41
+
42
+ private
43
+
44
+ def method_missing(method_name, *args, &block)
45
+ if request_methods.include?(method_name)
46
+ request(method_name, *args)
47
+ else
48
+ object.send(method_name, *args, &block)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ module MyJohnDeereApi
2
+ module NetHttpRetry
3
+ ##
4
+ # This error is used when a single request has exceeded
5
+ # the number of retries allowed by NetHttpRetry::Decorator::MAX_RETRIES.
6
+
7
+ class MaxRetriesExceededError < StandardError
8
+
9
+ ##
10
+ # argument is a string which describes the attempted request
11
+
12
+ def initialize(request_method, response_message)
13
+ message = "Max retries exceeded for #{request_method.to_s.upcase} " +
14
+ "request: #{response_message}"
15
+
16
+ super(message)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module MyJohnDeereApi
2
- VERSION='2.3.3'
2
+ VERSION='2.4.1'
3
3
  end
@@ -6,7 +6,7 @@ describe 'MyJohnDeereApi::Client' do
6
6
  assert_equal 'thisIsATest', client.send(:camelize, :this_is_a_test)
7
7
  end
8
8
 
9
- describe '#initialize(api_key, api_secret)' do
9
+ describe '#initialize(api_key, api_secret, options={})' do
10
10
  it 'sets the api key/secret' do
11
11
  client = JD::Client.new(api_key, api_secret)
12
12
 
@@ -35,6 +35,23 @@ describe 'MyJohnDeereApi::Client' do
35
35
  client = JD::Client.new(api_key, api_secret, contribution_definition_id: contribution_definition_id)
36
36
  assert_equal contribution_definition_id, client.contribution_definition_id
37
37
  end
38
+
39
+ it 'accepts a list of parameters for NetHttpRetry' do
40
+ custom_retries = JD::NetHttpRetry::Decorator::DEFAULTS[:max_retries] + 10
41
+
42
+ VCR.use_cassette('catalog') do
43
+ new_client = JD::Client.new(
44
+ api_key,
45
+ api_secret,
46
+ contribution_definition_id: contribution_definition_id,
47
+ environment: :sandbox,
48
+ access: [access_token, access_secret],
49
+ http_retry: {max_retries: custom_retries}
50
+ )
51
+
52
+ assert_equal custom_retries, new_client.accessor.max_retries
53
+ end
54
+ end
38
55
  end
39
56
 
40
57
  describe '#contribution_definition_id' do
@@ -130,7 +147,7 @@ describe 'MyJohnDeereApi::Client' do
130
147
 
131
148
  it 'sends the request' do
132
149
  response = VCR.use_cassette('put_asset') { client.put("/assets/#{asset_id}", attributes) }
133
- puts "HASH!! #{response.inspect}" if response.is_a?(Hash)
150
+
134
151
  assert_equal '204', response.code
135
152
  assert_equal 'No Content', response.message
136
153
  end
@@ -199,7 +216,7 @@ describe 'MyJohnDeereApi::Client' do
199
216
 
200
217
  describe '#accessor' do
201
218
  it 'returns an object that can make user-specific requests' do
202
- assert_kind_of OAuth::AccessToken, accessor
219
+ assert_kind_of JD::NetHttpRetry::Decorator, accessor
203
220
  assert_kind_of OAuth::Consumer, accessor.consumer
204
221
  assert_equal access_token, accessor.token
205
222
  assert_equal access_secret, accessor.secret