my_john_deere_api 2.3.3 → 2.4.1

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.
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