football-butler 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ac3e5bc4938cd010674e12b86aa82cadfe6e726e547bf74b819ba837513bc6be
4
+ data.tar.gz: 5d8ab6c615a5118b917b1ac11dcf92cccf657112d67472b678b2a1af9efa10b7
5
+ SHA512:
6
+ metadata.gz: c71e8b4d174abbb48a2c2810b0b963e63f6fd879772e77c1869b8fa6870fe59dc22ead56a898bfdbc65065e3e969930987d5c169667330fda36c6d043a90c7de
7
+ data.tar.gz: f94ff67c41f1cca54a54eea7c70dc3674dc6ef184868e6656ba3ac4d100704c8054026c9894c4ac9eb7dfa88c63a9dd2d7d712b7c6262607441dd8e1f174fcc0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ require 'httparty'
3
+ require "football/butler/version"
4
+ require "football/butler/base"
5
+ require "football/butler/configuration"
6
+ require "football/butler/tier"
7
+ require 'football/butler/api'
8
+ require 'football/butler/competitions'
9
+ require 'football/butler/matches'
10
+ require 'football/butler/areas'
11
+ require 'football/butler/teams'
12
+
13
+ module Football
14
+ module Butler
15
+ include Configuration
16
+ include Tier
17
+
18
+ class << self
19
+ def get(path:, filters: {}, result: :default)
20
+ Api.get(path: path, filters: filters, result: result)
21
+ end
22
+
23
+ def logger
24
+ @@logger ||= defined?(Rails) && Rails.logger ? Rails.logger : Logger.new(STDOUT)
25
+ end
26
+
27
+ def logger=(logger)
28
+ @@logger = logger
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+ require 'httparty'
3
+
4
+ module Football
5
+ module Butler
6
+ class Api < Base
7
+ RETRIES = 2
8
+
9
+ class << self
10
+ def get(path:, filters: {}, result: :default)
11
+ Configuration.wait_on_limit ?
12
+ get_with_wait(path: path, filters: filters, result: result) :
13
+ get_default(path: path, filters: filters, result: result)
14
+ end
15
+
16
+ private
17
+
18
+ def get_default(path:, filters: {}, result: :default)
19
+ return invalid_config_result if invalid_config?
20
+ response = process_http_party(path, filters)
21
+ process_response(response, result)
22
+ end
23
+
24
+ def get_with_wait(path:, filters: {}, result: :default)
25
+ return invalid_config_result if invalid_config?
26
+
27
+ if Tier.limit_exceeded?
28
+ log("Tier.limit_exceeded, sleeping for #{Tier.get_sleep_seconds} seconds now ...")
29
+ sleep(Tier.get_sleep_seconds) unless Rails.env.test?
30
+ end
31
+
32
+ response = process_http_party(path, filters)
33
+ Tier.set_from_response_headers(response)
34
+
35
+ if reached_limit?(response)
36
+ response = process_retry(path, filters)
37
+ end
38
+
39
+ process_response(response, result)
40
+ end
41
+
42
+ def process_retry(path, filters)
43
+ response = nil
44
+ retries = 0
45
+
46
+ while retries <= RETRIES
47
+ retries +=1
48
+
49
+ log("Tier.limit_exceeded again (retry #{retries}), sleeping for #{Tier.get_sleep_seconds} seconds now ...")
50
+ sleep(Tier.get_sleep_seconds) unless Rails.env.test?
51
+
52
+ response = process_http_party(path, filters)
53
+ Tier.set_from_response_headers(response)
54
+
55
+ break unless reached_limit?(response)
56
+ end
57
+ response
58
+ end
59
+
60
+ def process_http_party(path, filters)
61
+ headers = {
62
+ "X-Auth-Token": Configuration.api_token
63
+ }
64
+ url = "#{Configuration.api_endpoint}/#{path}"
65
+ query = filters || {}
66
+
67
+ http_party_get(url, headers, query)
68
+ end
69
+
70
+ def process_response(response, result)
71
+ if response.dig('message')
72
+ error_message(response['message'])
73
+ else
74
+ case result
75
+ when :default
76
+ response
77
+ else
78
+ response&.keys&.include?(result.to_s) ? response[result.to_s] : nil
79
+ end
80
+ end
81
+ end
82
+
83
+ def http_party_get(url, headers, query)
84
+ HTTParty.get "#{url}",
85
+ headers: headers,
86
+ query: query,
87
+ format: :json
88
+ end
89
+
90
+ def invalid_config?
91
+ Configuration.api_token.blank? || Configuration.api_endpoint.blank?
92
+ end
93
+
94
+ def log(text)
95
+ "\n\nFootball::Butler::VERSION: #{Football::Butler::VERSION} - #{text}\n\n"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ class Areas < Base
6
+ PATH = :areas
7
+
8
+ ## AREA
9
+ # v2/areas/{id}
10
+ # returns area object directly as a hash
11
+ def self.by_id(id:)
12
+ path = "#{PATH}/#{id}"
13
+ Api.get(path: path)
14
+ end
15
+
16
+ ## AREAS
17
+ # v2/areas
18
+ def self.all(result: PATH)
19
+ Api.get(path: PATH, result: result)
20
+ end
21
+
22
+ ## ADDITIONAL
23
+ # v2/areas
24
+ # v2/areas/{id}
25
+ # returns area object directly as a hash
26
+ def self.by_name(name:)
27
+ areas = all
28
+ return areas if areas.is_a?(Hash) && areas.with_indifferent_access.dig('message')
29
+ area = areas&.detect { |area| area['name'] == name }
30
+ return not_found_result(name) unless area
31
+
32
+ by_id(id: area['id'])
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ class Base
6
+ MSG_INVALID_TOKEN = 'Your API token is invalid.' # 400
7
+ MSG_NOT_EXIST = 'The resource you are looking for does not exist' # 404
8
+ MSG_REACHED_LIMIT = 'You reached your request limit.' # 429
9
+ MSG_INVALID_CONFIG = 'Invalid Configuration, check empty api_token / api_endpoint!'
10
+
11
+ class << self
12
+ # MESSAGES
13
+ def invalid_token?(response)
14
+ response.dig('message') ? response['message'] == MSG_INVALID_TOKEN : false
15
+ end
16
+
17
+ def resource_not_found?(response)
18
+ response.dig('message') ? response['message'] == MSG_NOT_EXIST : false
19
+ end
20
+
21
+ def reached_limit?(response)
22
+ response.dig('message') ? response['message'].start_with?(MSG_REACHED_LIMIT) : false
23
+ end
24
+
25
+ # CODES
26
+ def bad_request?(response)
27
+ response.dig('errorCode') ? response['errorCode'] == 400 : false
28
+ end
29
+
30
+ # RESULT MESSAGES
31
+ def not_found_result(*params)
32
+ error_message("#{params.join(', ')} could not be found.")
33
+ end
34
+
35
+ def invalid_config_result
36
+ error_message(MSG_INVALID_CONFIG)
37
+ end
38
+
39
+ def error_message(error)
40
+ { message: error }.with_indifferent_access
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ class Competitions < Base
6
+ PATH = :competitions
7
+
8
+ ## COMPETITION
9
+ # v2/competitions/{id}
10
+ # returns competition object directly as a hash
11
+ def self.by_id(id:)
12
+ path = "#{PATH}/#{id}"
13
+ Api.get(path: path)
14
+ end
15
+
16
+ ## COMPETITIONS
17
+ #
18
+ # areas={AREAS}
19
+ # plan={PLAN}
20
+ #
21
+ # v2/competitions
22
+ def self.all(result: PATH, filters: Configuration.tier_plan_filter)
23
+ Api.get(path: PATH, result: result, filters: filters)
24
+ end
25
+
26
+ # v2/competitions?plan={plan}
27
+ def self.by_plan(plan:, result: PATH, filters: {})
28
+ filters.merge!({ plan: plan })
29
+ Api.get(path: PATH, result: result, filters: filters)
30
+ end
31
+
32
+ # v2/competitions?areas={id1, id2, ...}
33
+ def self.by_areas(ids:, result: PATH, filters: {})
34
+ filters.merge!({ areas: ids.join(',') })
35
+ Api.get(path: PATH, result: result, filters: filters)
36
+ end
37
+
38
+ ## ADDITIONAL
39
+ # v2/competitions/{id}
40
+ def self.current_match_day(id:)
41
+ response = by_id(id:id)
42
+
43
+ if response.is_a?(Hash) && response.dig('message')
44
+ response
45
+ else
46
+ response['currentSeason']['currentMatchday']
47
+ end
48
+ end
49
+
50
+ # v2/competitions/{id}
51
+ def self.seasons(id:)
52
+ response = by_id(id:id)
53
+
54
+ if response.is_a?(Hash) && response.dig('message')
55
+ response
56
+ else
57
+ response['seasons']
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ module Configuration
6
+ # API
7
+ DEFAULT_API_URL = "https://api.football-data.org"
8
+
9
+ DEFAULT_API_TOKEN = nil
10
+ DEFAULT_API_VERSION = 2
11
+ DEFAULT_API_ENDPOINT = "#{DEFAULT_API_URL}/v#{DEFAULT_API_VERSION}"
12
+
13
+ # ADDITIONAL
14
+ DEFAULT_TIER_PLAN = nil
15
+ DEFAULT_WAIT_ON_LIMIT = false
16
+
17
+ class << self
18
+ attr_accessor :api_version, :api_token, :api_endpoint, :tier_plan, :wait_on_limit, :init_done
19
+
20
+ def configure
21
+ raise "You need to configure football-butler first, see readme." unless block_given?
22
+
23
+ yield self
24
+
25
+ @api_token ||= DEFAULT_API_TOKEN
26
+ @api_version ||= DEFAULT_API_VERSION
27
+ @api_endpoint ||= DEFAULT_API_ENDPOINT
28
+ @tier_plan ||= DEFAULT_TIER_PLAN
29
+ @wait_on_limit ||= DEFAULT_WAIT_ON_LIMIT
30
+
31
+ @init_done = true
32
+
33
+ true
34
+ end
35
+
36
+ def reconfigure(
37
+ api_token: nil, api_version: nil, api_endpoint: nil, tier_plan: nil, wait_on_limit: nil
38
+ )
39
+
40
+ reset unless @init_done
41
+
42
+ @api_token = api_token unless api_token.nil?
43
+ unless api_version.nil?
44
+ @api_version = api_version
45
+ @api_endpoint = "#{DEFAULT_API_URL}/v#{api_version}" if api_endpoint.nil?
46
+ end
47
+ @api_endpoint = api_endpoint unless api_endpoint.nil?
48
+ @tier_plan = tier_plan unless tier_plan.nil?
49
+ @wait_on_limit = wait_on_limit unless wait_on_limit.nil?
50
+
51
+ true
52
+ end
53
+
54
+ def reset
55
+ @api_version = DEFAULT_API_VERSION
56
+ @api_endpoint = DEFAULT_API_ENDPOINT
57
+ @tier_plan = DEFAULT_TIER_PLAN
58
+ @wait_on_limit = DEFAULT_WAIT_ON_LIMIT
59
+
60
+ @init_done = true
61
+
62
+ true
63
+ end
64
+
65
+ # plan = [ TIER_ONE | TIER_TWO | TIER_THREE | TIER_FOUR ]
66
+ def tier_plan_filter
67
+ tier_plan.nil? ? {} : { plan: tier_plan }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ class Matches < Base
6
+ PATH = :matches
7
+ STATUS_SCHEDULED = 'SCHEDULED'
8
+ STATUS_FINISHED = 'FINISHED'
9
+
10
+ ## MATCH
11
+ # v2/matches/{id}
12
+ # returns match object directly as a hash
13
+ def self.by_id(id:)
14
+ path = "#{PATH}/#{id}"
15
+ Api.get(path: path)
16
+ end
17
+
18
+ ## MATCHES
19
+ #
20
+ # competitions={competitionIds}
21
+ # dateFrom={DATE}
22
+ # dateTo={DATE}
23
+ # status={STATUS}
24
+ #
25
+ # /v2/matches
26
+ def self.all(result: PATH, filters: {})
27
+ Api.get(path: PATH, result: result, filters: filters)
28
+ end
29
+
30
+ ## by COMPETITION
31
+ #
32
+ # dateFrom={DATE}
33
+ # dateTo={DATE}
34
+ # stage={STAGE}
35
+ # status={STATUS}
36
+ # matchday={MATCHDAY}
37
+ # group={GROUP}
38
+ # season={YEAR}
39
+ #
40
+ # v2/competitions/{id}/matches
41
+ def self.by_competition(id:, result: PATH, filters: {})
42
+ path = "#{Competitions::PATH}/#{id}/#{PATH}"
43
+ Api.get(path: path, filters: filters, result: result)
44
+ end
45
+
46
+ # v2/competitions/{id}/matches?season={year}
47
+ def self.by_competition_and_year(id:, year:, result: PATH, filters: {})
48
+ path = "#{Competitions::PATH}/#{id}/#{PATH}"
49
+ filters.merge!({ season: year })
50
+ Api.get(path: path, filters: filters, result: result)
51
+ end
52
+
53
+ # v2/competitions/{id}/matches?matchday={match_day}
54
+ def self.by_competition_and_match_day(id:, match_day:, result: PATH, filters: {})
55
+ path = "#{Competitions::PATH}/#{id}/#{PATH}"
56
+ filters.merge!({ matchday: match_day })
57
+ Api.get(path: path, filters: filters, result: result)
58
+ end
59
+
60
+ ## by TEAM
61
+ #
62
+ # dateFrom={DATE}
63
+ # dateTo={DATE}
64
+ # status={STATUS}
65
+ # venue={VENUE}
66
+ # limit={LIMIT}
67
+ #
68
+ # v2/teams/{id}/matches
69
+ def self.by_team(id:, result: PATH, filters: {})
70
+ path = "#{Teams::PATH}/#{id}/#{PATH}"
71
+ Api.get(path: path, result: result, filters: filters)
72
+ end
73
+
74
+ # v2/teams/{id}/matches?status={status}
75
+ def self.by_team_and_status(id:, status:, result: PATH, filters: {})
76
+ path = "#{Teams::PATH}/#{id}/#{PATH}"
77
+ filters.merge!({ status: status })
78
+ Api.get(path: path, result: result, filters: filters)
79
+ end
80
+
81
+ # v2/teams/{team}/matches?status=FINISHED
82
+ def self.by_team_finished(id:, result: PATH, filters: {})
83
+ by_team_and_status(id: id, status: STATUS_FINISHED, result: result, filters: filters)
84
+ end
85
+
86
+ # v2/teams/{team}/matches?status=SCHEDULED
87
+ def self.by_team_scheduled(id:, result: PATH, filters: {})
88
+ by_team_and_status(id: id, status: STATUS_SCHEDULED, result: result, filters: filters)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ class Teams < Base
6
+ PATH = :teams
7
+
8
+ ## TEAM
9
+ # v2/teams/{id}
10
+ # returns team object directly as a hash
11
+ def self.by_id(id:)
12
+ path = "#{PATH}/#{id}"
13
+ Api.get(path: path)
14
+ end
15
+
16
+ ## COMPETITION
17
+ #
18
+ # season={YEAR}
19
+ # stage={STAGE}
20
+ #
21
+ # v2/competitions/{id}/teams
22
+ def self.by_competition(id:, result: PATH, filters: {})
23
+ path = "#{Competitions::PATH}/#{id}/#{PATH}"
24
+ Api.get(path: path, result: result, filters: filters)
25
+ end
26
+
27
+ # v2/competitions/{id}/teams?year={year}
28
+ def self.by_competition_and_year(id:, year:, result: PATH, filters: {})
29
+ path = "#{Competitions::PATH}/#{id}/#{PATH}"
30
+ filters.merge!({ year: year })
31
+ Api.get(path: path, result: result, filters: filters)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ module Tier
6
+ COUNTER_RESET_SAFE_SECONDS = 5
7
+
8
+ class << self
9
+ attr_accessor :available_minute, :counter_reset, :last_request, :total_requests, :sleep_seconds
10
+
11
+ def set_from_response_headers(response)
12
+ if available_minute?(response) && counter_reset?(response)
13
+ set_tier_from_response(
14
+ response.headers['x-requests-available-minute'],
15
+ response.headers['x-requestcounter-reset']
16
+ )
17
+ end
18
+ end
19
+
20
+ def set_tier_from_response(available_minute, counter_reset)
21
+ @available_minute = available_minute.to_i
22
+ @counter_reset = counter_reset.to_i
23
+ @last_request = Time.current
24
+
25
+ @total_requests = @total_requests.is_a?(Integer) ? @total_requests + 1 : 1
26
+
27
+ true
28
+ end
29
+
30
+ def available_minute?(response)
31
+ response&.headers&.dig('x-requests-available-minute')&.present?
32
+ end
33
+
34
+ def counter_reset?(response)
35
+ response&.headers&.dig('x-requestcounter-reset')&.present?
36
+ end
37
+
38
+ def get_sleep_seconds
39
+ seconds = @counter_reset.is_a?(Integer) ? @counter_reset : 60
40
+ result = COUNTER_RESET_SAFE_SECONDS + seconds
41
+
42
+ @sleep_seconds = @sleep_seconds.is_a?(Integer) ?
43
+ @sleep_seconds + result : result
44
+
45
+ result
46
+ end
47
+
48
+ def limit_exceeded?
49
+ @available_minute == 0 && @last_request >= 1.minute.ago
50
+ end
51
+
52
+ def reset_total_requests
53
+ @total_requests = 0
54
+ end
55
+
56
+ def reset_sleep_seconds
57
+ @sleep_seconds = 0
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Football
4
+ module Butler
5
+ # TIER_ONE initial Version April 2021
6
+ VERSION = "1.0.0"
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: football-butler
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jörg Kirschstein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: byebug
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: httparty
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.15.7
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 0.15.7
69
+ description: Get data from https://www.football-data.org
70
+ email:
71
+ - info@joerg-kirschstein.de
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/football/butler.rb
77
+ - lib/football/butler/api.rb
78
+ - lib/football/butler/areas.rb
79
+ - lib/football/butler/base.rb
80
+ - lib/football/butler/competitions.rb
81
+ - lib/football/butler/configuration.rb
82
+ - lib/football/butler/matches.rb
83
+ - lib/football/butler/teams.rb
84
+ - lib/football/butler/tier.rb
85
+ - lib/football/butler/version.rb
86
+ homepage: https://github.com/frontimax/football-butler
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/frontimax/football-butler
91
+ source_code_uri: https://rubygems.org/gems/football_butler
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 2.3.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.1.2
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Football data via API
111
+ test_files: []