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.
- checksums.yaml +4 -4
- data/README.md +236 -3
- data/lib/my_john_deere_api.rb +1 -0
- data/lib/my_john_deere_api/client.rb +13 -3
- data/lib/my_john_deere_api/net_http_retry.rb +2 -0
- data/lib/my_john_deere_api/net_http_retry/decorator.rb +53 -0
- data/lib/my_john_deere_api/net_http_retry/max_retries_exceeded_error.rb +20 -0
- data/lib/my_john_deere_api/version.rb +1 -1
- data/test/lib/my_john_deere_api/client_test.rb +20 -3
- data/test/lib/my_john_deere_api/errors/max_retries_exceeded_error_test.rb +13 -0
- data/test/lib/my_john_deere_api/net_http_retry/decorator_test.rb +163 -0
- data/test/support/helper.rb +1 -0
- data/test/support/vcr/accessor/delete_failed.yml +327 -0
- data/test/support/vcr/accessor/delete_max_failed.yml +615 -0
- data/test/support/vcr/accessor/delete_retry.yml +191 -0
- data/test/support/vcr/accessor/delete_retry_too_soon.yml +191 -0
- data/test/support/vcr/accessor/get_failed.yml +390 -0
- data/test/support/vcr/accessor/get_max_failed.yml +734 -0
- data/test/support/vcr/accessor/get_retry.yml +226 -0
- data/test/support/vcr/accessor/get_retry_too_soon.yml +226 -0
- data/test/support/vcr/accessor/post_failed.yml +417 -0
- data/test/support/vcr/accessor/post_max_failed.yml +785 -0
- data/test/support/vcr/accessor/post_retry.yml +241 -0
- data/test/support/vcr/accessor/post_retry_too_soon.yml +241 -0
- data/test/support/vcr/accessor/put_failed.yml +372 -0
- data/test/support/vcr/accessor/put_max_failed.yml +700 -0
- data/test/support/vcr/accessor/put_retry.yml +216 -0
- data/test/support/vcr/accessor/put_retry_too_soon.yml +216 -0
- metadata +24 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75b86b9b093f4aa0cc586ce56fb961654b1b91577947acf48392d8d2294c85b9
|
4
|
+
data.tar.gz: b5879b20c85891180747ba42657ffa399da981596820c813a7049d680240dd39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a8c729311baa853f8ae1039573f673cb01cc084d0fc9b662ce592dd1b907a85b153f04d07ebd6bf346558efb708242f5c13f3464f3c4c5b03a4c21a2479c2d9
|
7
|
+
data.tar.gz: cf5dd0b1e590baeb06ead7ffeffb4a7501de17b8b56596a297797186f1bc85aa33b196f58f19162f6027e40a27b023f9ad18ad3aa25e07b61fcd383612c0bbaa
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[](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
|
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' => '
|
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
|
data/lib/my_john_deere_api.rb
CHANGED
@@ -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
|
-
|
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,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
|
@@ -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
|
-
|
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
|
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
|