meteofrance-api 0.1.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.
@@ -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