meteofrance-api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ require "meteofrance_api/constants"
2
+
3
+ module MeteofranceApi::Helpers
4
+ # Convert the color code in readable text.
5
+ #
6
+ # Args:
7
+ # color_code: Color status in int. Value expected between 1 and 4.
8
+ # lang(Optional): If language is equal :fr (default value) results will
9
+ # be in French. All other value will give results in English.
10
+
11
+ #Returns:
12
+ #Color status in text. French or English according to the lang parameter.
13
+ def color_code_to_str(
14
+ code,
15
+ lang = :fr
16
+ )
17
+ colors = MeteofranceApi::ALERT_COLORS[lang] || MeteofranceApi::ALERT_COLORS[:en]
18
+
19
+ colors[code]
20
+ end
21
+
22
+
23
+ # Convert the phenomenom code in readable text (Hepler).
24
+ #
25
+ # Args:
26
+ # code: ID of the phenomenom in int. Value expected between 1 and 9.
27
+ # lang: Optional; If language is equal :fr (default value) results will
28
+ # be in French. All other value will give results in English.
29
+ #
30
+ # Returns:
31
+ # Phenomenom in text. French or English according to the lang parameter.
32
+ def alert_code_to_str(
33
+ code,
34
+ lang = :fr
35
+ )
36
+ alert_types = MeteofranceApi::ALERT_TYPES[lang] || MeteofranceApi::ALERT_TYPES[:en]
37
+
38
+ alert_types[code]
39
+ end
40
+
41
+
42
+ # Identify when a second bulletin is availabe for coastal risks (Helper).
43
+ #
44
+ # Args:
45
+ # department_number: Department number on 2 characters
46
+ #
47
+ # Returns:
48
+ # True if the department have an additional coastal bulletin. False otherwise.
49
+ #
50
+ def is_coastal_department?(department_number)
51
+ MeteofranceApi::COASTAL_DEPARTMENTS.include?(department_number)
52
+ end
53
+
54
+
55
+ # Identify if there is a weather alert bulletin for this department (Helper).
56
+ #
57
+ # Weather alert buletins are available only for metropolitan France and Andorre.
58
+ #
59
+ # Args:
60
+ # department_number: Department number on 2 characters.
61
+ #
62
+ # Returns:
63
+ # True if a department is metropolitan France or Andorre.
64
+ #
65
+ def is_department?(department_number)
66
+ MeteofranceApi::VALID_DEPARTMENTS.include?(department_number)
67
+ end
68
+
69
+ # Compute distance in meters between to GPS coordinates using Harvesine formula.
70
+ #
71
+ # source: https://janakiev.com/blog/gps-points-distance-python/
72
+ #
73
+ # Args:
74
+ # coord1: Tuple with latitude and longitude in degrees for first point
75
+ # coord2: Tuple with latitude and longitude in degrees for second point
76
+ #
77
+ # Returns:
78
+ # Distance in meters between the two points
79
+ def haversine(coord1, coord2)
80
+ to_radians = ->(v) { v * (Math::PI / 180) }
81
+ radius = 6372800 # Earth radius in meters
82
+
83
+ lat1, lon1 = coord1
84
+ lat2, lon2 = coord2
85
+
86
+ phi1, phi2 = to_radians.call(lat1), to_radians.call(lat2)
87
+ dphi = to_radians.call(lat2 - lat1)
88
+ dlambda = to_radians.call(lon2 - lon1)
89
+
90
+ a = (
91
+ Math.sin(dphi / 2) ** 2
92
+ + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dlambda / 2) ** 2
93
+ )
94
+
95
+ return 2 * radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
96
+ end
97
+
98
+
99
+ # Order list of places according to the distance to a reference coordinates.
100
+ #
101
+ # Note: this helper is compensating the bad results of the API. Results in the API
102
+ # are generally sorted, but lot of cases identified where the order is inconsistent
103
+ # (example: Montréal)
104
+ #
105
+ # Args:
106
+ # list_places: List of Place instances to be ordered
107
+ # gps_coord: Tuple with latitude and longitude in degrees for the reference point
108
+ #
109
+ # Returns:
110
+ # List of Place instances ordered by distance to the reference point (nearest
111
+ # first)
112
+ #
113
+ def sort_places_versus_distance_from_coordinates(
114
+ places,
115
+ gps_coord
116
+ )
117
+ places.sort_by {|place| haversine(place.latitude.to_i, place.longitude.to_i, gps_coord)}
118
+ end
119
+ end
@@ -0,0 +1,38 @@
1
+ class MeteofranceApi::Place
2
+
3
+ # INSEE ID of the place.
4
+ attr_reader :insee
5
+ # name of the place.
6
+ attr_reader :name
7
+ # latitude of the place.
8
+ attr_reader :latitude
9
+ # longitude of the place.
10
+ attr_reader :longitude
11
+ # country code of the place.
12
+ attr_reader :country
13
+ # Province/Region of the place.
14
+ attr_reader :province
15
+ # Province/Region's numerical code of the place.
16
+ attr_reader :province_code
17
+ # postal code of the place.
18
+ attr_reader :postal_code
19
+
20
+ def initialize(data)
21
+ @insee = data["insee"]
22
+ @name = data["name"]
23
+ @latitude = data["lat"]
24
+ @longitude = data["lon"]
25
+ @country = data["country"]
26
+ @province = data["admin"]
27
+ @province_code = data["admin2"]
28
+ @postal_code = data["postCode"]
29
+ end
30
+
31
+ def to_s
32
+ if country == "FR"
33
+ "#{name} - #{admin} (#{admin2}) - #{country}"
34
+ else
35
+ "#{name} - #{admin} - #{country}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ class MeteofranceApi::Rain::Forecast
2
+ attr_reader :time
3
+ attr_reader :intensity
4
+ attr_reader :description
5
+
6
+ def intensity(data)
7
+ @time = Time.parse(data["time"])
8
+ @intensity = data["rain_intensity"]
9
+ @description = data["rain_intensity_description"]
10
+ end
11
+ end
@@ -0,0 +1,44 @@
1
+ class MeteofranceApi::Rain
2
+ # update time of the rain forecast.
3
+ attr_reader :updated_on
4
+ # position information of the rain forecast ([latitude, longitude]).
5
+ attr_reader :place_name
6
+ attr_reader :position
7
+ attr_reader :altitude
8
+ attr_reader :french_department
9
+ attr_reader :timezone
10
+ # the rain forecast.
11
+ attr_reader :forecasts
12
+ # quality of the rain forecast.
13
+ attr_reader :confidence
14
+
15
+ def initialize(data)
16
+ @updated_on = Time.at(data["updated_time"]).utc
17
+ @position = data["geometry"]["coordinates"]
18
+ @place_name = data["properties"]["name"]
19
+ @altitude = data["properties"]["altitude"]
20
+ @french_department = data["properties"]["french_department"]
21
+ @timezone = data["properties"]["timezone"]
22
+ @forecasts = data["properties"]["forecast"].map {|d| MeteofranceApi::Rain::Forecast.new(d)}
23
+ @confidence = data["properties"]["confidence"]
24
+ end
25
+
26
+ def to_locale_timezone(time)
27
+ time.in_time_zone(position["timezone"])
28
+ end
29
+
30
+ # Estimate the date of the next rain.
31
+ #
32
+ # Returns:
33
+ # A datetime instance representing the date estimation of the next rain within
34
+ # the next hour.
35
+ # If no rain is expected in the following hour nil is returned.
36
+ def next_rain_date(use_position_timezone)
37
+ # search first cadran with rain
38
+ next_rain = forecasts.find {|f| f.intensity > 1}
39
+
40
+ next_rain&.time
41
+ end
42
+ end
43
+
44
+ require "meteofrance_api/models/rain/forecast"
@@ -0,0 +1,44 @@
1
+ # Class to access the results of a `warning/currentPhenomenons` REST API request.
2
+ #
3
+ # For coastal department two bulletins are avalaible corresponding to two different
4
+ # domains.
5
+ class MeteofranceApi::Warning::Current
6
+ # update time of the phenomenoms.
7
+ attr_reader :update_time
8
+ # end of validty time of the phenomenoms.
9
+ attr_reader :end_validity_time
10
+ # domain ID of the phenomenons. Value is 'France' or a department number
11
+ attr_reader :domain_id
12
+ # List of Phenomenon
13
+ attr_reader :phenomenons
14
+
15
+ def initialize(data)
16
+ @update_time = Time.at(data["update_time"])
17
+ @end_validity_time = Time.at(data["end_validity_time"])
18
+ @domain_id = data["domain_id"]
19
+ @phenomenons = (data["phenomenons_max_colors"] || []).map {|i| Warning::Phenomenon.new(i)}
20
+ end
21
+
22
+ # Merge the classical phenomenoms bulleting with the coastal one.
23
+
24
+ # Extend the phenomenomes_max_colors property with the content of the coastal
25
+ # weather alert bulletin.
26
+
27
+ # Args:
28
+ # coastal_phenomenoms: WarningCurrentPhenomenons instance corresponding to the
29
+ # coastal weather alert bulletin.
30
+ #
31
+ def merge_with_coastal_phenomenons!(coastal_phenomenons)
32
+ # TODO: Add consitency check
33
+ @phenomenons += coastal_phenomenoms.phenomenons
34
+ end
35
+
36
+ # Get the maximum level of alert of a given domain (class helper).
37
+ # Returns:
38
+ # An integer corresponding to the status code representing the maximum alert.
39
+ def get_domain_max_color
40
+ phenomenons.map {|item| item.max_color_id}.max
41
+ end
42
+ end
43
+
44
+ require "meteofrance_api/models/warning/phenomenon"
@@ -0,0 +1,67 @@
1
+ # This class allows to access the results of a `warning/full` API command.
2
+ #
3
+ # For a given domain we can access the maximum alert, a timelaps of the alert
4
+ # evolution for the next 24 hours, and a list of alerts.
5
+ #
6
+ # For coastal department two bulletins are avalaible corresponding to two different
7
+ # domains.
8
+ class MeteofranceApi::Warning::Full
9
+ # update time of the full bulletin.
10
+ attr_reader :update_time
11
+ # end of validty time of the full bulletin.
12
+ attr_reader :end_validity_time
13
+ # domain ID of the the full bulletin. Value is 'France' or a department number.
14
+ attr_reader :domain_id
15
+ # color max of the domain.
16
+ attr_reader :color_max
17
+ # timelaps of each phenomenom for the domain. A list of Hash corresponding to the schedule of each phenomenons in the next 24 hours
18
+ attr_reader :timelaps
19
+ # phenomenom list of the domain.
20
+ attr_reader :phenomenons
21
+
22
+ attr_reader :advices
23
+ attr_reader :consequences
24
+ attr_reader :max_count_items
25
+ attr_reader :comments
26
+ attr_reader :text
27
+ attr_reader :text_avalanche
28
+
29
+ def initialize(data)
30
+ @update_time = Time.at(data["update_time"])
31
+ @end_validity_time = Time.at(data["end_validity_time"])
32
+ @domain_id = data["domain_id"]
33
+ @color_max = data["color_max"]
34
+ @timelaps = data["timelaps"]
35
+ @phenomenons = (data["phenomenons_items"] || []).map {|i| MeteofranceApi::Warning::Phenomenon.new(i)}
36
+
37
+ @advices = data["advices"]
38
+ @consequences = data["consequences"]
39
+ @max_count_items = data["max_count_items"]
40
+ @comments = data["comments"]
41
+ @text = data["text"]
42
+ @text["text_avalanche"]
43
+ end
44
+
45
+ # Merge the classical phenomenon bulletin with the coastal one.
46
+
47
+ # Extend the color_max, timelaps and phenomenons properties with the content
48
+ # of the coastal weather alert bulletin.
49
+
50
+ # Args:
51
+ # coastal_phenomenoms: Full instance corresponding to the coastal weather
52
+ # alert bulletin.
53
+ def merge_with_coastal_phenomenons!(coastal_phenomenons)
54
+ # TODO: Add consitency check
55
+ # TODO: Check if other data need to be merged
56
+
57
+ @color_max = [color_max, coastal_phenomenons.color_max].max
58
+
59
+ # Merge timelaps
60
+ @timelaps += coastal_phenomenons.timelaps
61
+
62
+ # Merge phenomenons
63
+ @phenomenons += coastal_phenomenons.phenomenons
64
+ end
65
+ end
66
+
67
+ require "meteofrance_api/models/warning/phenomenon"
@@ -0,0 +1,9 @@
1
+ class MeteofranceApi::Warning::Phenomenon
2
+ attr_reader :id
3
+ attr_reader :max_color_id
4
+
5
+ def initialize(data)
6
+ @id = data["phenomenon_id"]
7
+ @max_color_id = data["phenomenon_max_color_id"]
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module MeteofranceApi::Warning
2
+ end
3
+
4
+ require "meteofrance_api/models/warning/current"
5
+ require "meteofrance_api/models/warning/full.rb"
@@ -0,0 +1,35 @@
1
+ class MeteofranceApi::Weather::DailyForecast
2
+ attr_reader :time
3
+
4
+ attr_reader :T_max
5
+ attr_reader :T_min
6
+ attr_reader :T_sea
7
+
8
+ attr_reader :relative_humidity_max
9
+ attr_reader :relative_humidity_min
10
+
11
+ attr_reader :daily_weather_description
12
+ attr_reader :daily_weather_icon
13
+
14
+ attr_reader :sunrise_time
15
+ attr_reader :sunset_time
16
+
17
+ attr_reader :total_precipitation_24h
18
+
19
+ attr_reader :uv_index
20
+
21
+ def initialize(data)
22
+ @time = Time.parse(data["time"])
23
+ @temperature_max = data["T_max"]
24
+ @temperature_min = data["T_min"]
25
+ @temperature_sea = data["T_sea"]
26
+ @relative_humidity_max = data["relative_humidity_max"]
27
+ @relative_humidity_min = data["relative_humidity_min"]
28
+ @weather_description = data["daily_weather_description"]
29
+ @weather_icon = data["daily_weather_icon"]
30
+ @sunrise_time = Time.parse(data["sunrise_time"])
31
+ @sunset_time = Time.parse(data["sunset_time"])
32
+ @total_precipitation_24h = data["total_precipitation_24h"]
33
+ @uv_index = data["uv_index"]
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ class MeteofranceApi::Weather::Forecast
2
+ attr_reader :time
3
+
4
+ attr_reader :temperature
5
+ attr_reader :temperature_windchill
6
+ attr_reader :relative_humidity
7
+
8
+ attr_reader :total_cloud_cover
9
+
10
+ attr_reader :weather_description
11
+ attr_reader :weather_icon
12
+
13
+ attr_reader :wind_direction
14
+ attr_reader :wind_icon
15
+ attr_reader :wind_speed
16
+ attr_reader :wind_speed_gust
17
+
18
+ attr_reader :rain_1h
19
+ attr_reader :rain_3h
20
+ attr_reader :rain_6h
21
+ attr_reader :rain_12h
22
+ attr_reader :rain_24h
23
+
24
+ attr_reader :rain_snow_limit
25
+
26
+ attr_reader :snow_1h
27
+ attr_reader :snow_3h
28
+ attr_reader :snow_6h
29
+ attr_reader :snow_12h
30
+ attr_reader :snow_24h
31
+
32
+ attr_reader :P_sea
33
+
34
+ attr_reader :iso0
35
+
36
+
37
+ def initialize(data)
38
+ @time = Time.parse(data["time"])
39
+ @temperature = data["T"]
40
+ @temperature_windchill = data["T_windchill"]
41
+ @relative_humidity = data["relative_humidity"]
42
+ @P_sea = data["P_sea"]
43
+ @wind_speed = data["wind_speed"]
44
+ @wind_speed_gust = data["wind_speed_gust"]
45
+ @wind_direction = data["wind_direction"]
46
+ @wind_icon = data["wind_icon"]
47
+ @rain_1h = data["rain_1h"]
48
+ @rain_3h = data["rain_3h"]
49
+ @rain_6h = data["rain_6h"]
50
+ @rain_12h = data["rain_12h"]
51
+ @rain_24h = data["rain_24h"]
52
+ @snow_1h = data["snow_1h"]
53
+ @snow_3h = data["snow_3h"]
54
+ @snow_6h = data["snow_6h"]
55
+ @snow_12h = data["snow_12h"]
56
+ @snow_24h = data["snow_24h"]
57
+ @iso0 = data["iso0"]
58
+ @rain_snow_limit = data["rain_snow_limit"]
59
+ @total_cloud_cover = data["total_cloud_cover"]
60
+ @weather_icon = data["weather_icon"]
61
+ @weather_description = data["weather_description"]
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ class MeteofranceApi::Weather::HazardForecast
2
+ attr_reader :rain_hazard_3h
3
+ attr_reader :rain_hazard_6h
4
+ attr_reader :snow_hazard_3h
5
+ attr_reader :snow_hazard_6h
6
+ attr_reader :freezing_hazard
7
+ attr_reader :storm_hazard
8
+
9
+ def initialize(data)
10
+ @rain_hazard_3h = data["rain_hazard_3h"]
11
+ @rain_hazard_6h = data["rain_hazard_6h"]
12
+ @snow_hazard_3h = data["snow_hazard_3h"]
13
+ @snow_hazard_6h = data["snow_hazard_6h"]
14
+ @freezing_hazard = data["freezing_hazard"]
15
+ @storm_hazard = data["storm_hazard"]
16
+ end
17
+ end
@@ -0,0 +1,89 @@
1
+ class MeteofranceApi::Weather
2
+ # update Time of the forecast.
3
+ attr_reader :updated_at
4
+
5
+ # position information of the forecast. An Array [longitude, latitude].
6
+ attr_reader :position
7
+ # Altitude of the position (in meter)
8
+ attr_reader :altitude
9
+ # Name of the position
10
+ attr_reader :name
11
+ # Country of the position
12
+ attr_reader :country
13
+ attr_reader :timezone
14
+ attr_reader :insee
15
+ attr_reader :french_department_code
16
+
17
+ attr_reader :bulletin_cote
18
+
19
+ # daily forecast for the following days.
20
+ # A list of Hash to describe the daily forecast for the next 15 days.
21
+ attr_reader :daily_forecasts
22
+ # hourly forecast.
23
+ # A list of Hash to describe the hourly forecast for the next day
24
+ attr_reader :forecasts
25
+ # wheather event forecast.
26
+ # A list of object to describe the event probability forecast (rain, snow, freezing) for next 10 days.
27
+ attr_reader :hazard_forecasts
28
+
29
+
30
+ def initialize(data)
31
+ @updated_on = Time.at(data["updated_time"])
32
+
33
+ @position = data["geometry"]["coordinates"]
34
+ @altitude = data["properties"]["altitude"]
35
+ @name = data["properties"]["name"]
36
+ @country = data["properties"]["country"]
37
+ @timezone = data["properties"]["timezone"]
38
+ @insee = data["properties"]["insee"]&.to_i
39
+ @french_department_code = data["properties"]["french_department_code"]&.to_i
40
+
41
+ @bulletin_cote = data["properties"]["bulletin_cote"]
42
+
43
+ @daily_forecasts = data.dig("daily_forecast", []).map {|d| MeteofranceApi::Weather::DailyForecast.new(d)}
44
+ @forecasts = data.dig("forecast", []).map {|d| MeteofranceApi::Weather::Forecast.new(d)}
45
+ @hazard_forecasts = data.dig("probability_forecast", []).map {|d| MeteofranceApi::Weather::HazardForecast.new(d)}
46
+ end
47
+
48
+ # Return the forecast for today. A Hash corresponding to the daily forecast for the current day
49
+ def today_forecast
50
+ self.daily_forecasts.first
51
+ end
52
+
53
+ # Return the nearest hourly forecast. A Hash corresponding to the nearest hourly forecast.
54
+ def nearest_forecast
55
+ # get timestamp for current time
56
+ now_timestamp = Time.now
57
+
58
+ # sort list of forecast by distance between current timestamp and
59
+ # forecast timestamp
60
+ sorted_forecasts = self.forecasts.sort_by {|f| (f.time - now_timestamp).abs }
61
+
62
+ sorted_forecasts.first
63
+ end
64
+
65
+ # Return the forecast of the current hour. A Hash corresponding to the hourly forecast for the current hour
66
+ def current_forecast
67
+ # Get the timestamp for the current hour.
68
+ current_hour = Time.now.utc.to_a
69
+ current_hour[0] = 0
70
+ current_hour[1] = 0
71
+ current_hour_timestamp = Time.utc.new(*current_hour).to_i
72
+
73
+ # create a Hash using timestamp as keys
74
+ forecasts_by_datetime = self.forecasts.map {|f| [f.time.to_i, f]}.to_h
75
+
76
+ # Return the forecast corresponding to the timestamp of the current hour if
77
+ # exists.
78
+ forecasts_by_datetime[current_hour_timestamp]
79
+ end
80
+
81
+ # Convert time in the forecast location timezone
82
+ def to_locale_time(time)
83
+ time.in_time_zone(timezone)
84
+ end
85
+ end
86
+
87
+ require "meteofrance_api/models/weather/forecast"
88
+ require "meteofrance_api/models/weather/daily_forecast"
89
+ require "meteofrance_api/models/weather/hazard_forecast"
@@ -0,0 +1,4 @@
1
+ require "meteofrance_api/models/weather"
2
+ require "meteofrance_api/models/place"
3
+ require "meteofrance_api/models/rain"
4
+ require "meteofrance_api/models/warning"
@@ -0,0 +1,3 @@
1
+ module MeteofranceApi
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,10 @@
1
+ module MeteofranceApi
2
+ class Error < StandardError; end
3
+ # Your code goes here...
4
+ end
5
+
6
+ require "meteofrance_api/client"
7
+ require "meteofrance_api/constants"
8
+ require "meteofrance_api/helpers"
9
+ require "meteofrance_api/models"
10
+ require "meteofrance_api/version"
@@ -0,0 +1,44 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "meteofrance_api/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "meteofrance-api"
8
+ spec.version = MeteofranceApi::VERSION
9
+ spec.authors = ["Thomas Kuntz"]
10
+ spec.email = ["thomaskuntz67@gmail.com"]
11
+
12
+ spec.summary = %q{Wrapper around meteofrance.com (french weather website) API.}
13
+ spec.description = %q{Wrapper around meteofrance.com non-public API. Based on Python lib https://github.com/hacf-fr/meteofrance-api}
14
+ spec.homepage = "https://github.com/Haerezis/meteofrance-api"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ #spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = "https://github.com/Haerezis/meteofrance-api"
24
+ spec.metadata["changelog_uri"] = "https://github.com/Haerezis/meteofrance-api/blob/main/CHANGELOG.md"
25
+ else
26
+ raise "RubyGems 2.0 or newer is required to protect against " \
27
+ "public gem pushes."
28
+ end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_dependency "faraday", "~> 2.3.0"
40
+
41
+ spec.add_development_dependency "bundler", "~> 1.17"
42
+ spec.add_development_dependency "rake", "~> 10.0"
43
+ spec.add_development_dependency "minitest", "~> 5.0"
44
+ end