footballdata-api 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,189 @@
1
+ module Footballdata
2
+
3
+
4
+ def self.assert( cond, msg )
5
+ if cond
6
+ # do nothing
7
+ else
8
+ puts "!!! assert failed - #{msg}"
9
+ exit 1
10
+ end
11
+ end
12
+
13
+ def self.fmt_competition( rec )
14
+ buf = String.new
15
+
16
+ buf << "==> "
17
+ buf << "#{rec['competition']['name']} (#{rec['competition']['code']}) -- "
18
+ buf << "#{rec['area']['name']} (#{rec['area']['code']}) "
19
+ buf << "#{rec['competition']['type']} "
20
+ buf << "#{rec['season']['startDate']} - #{rec['season']['endDate']} "
21
+ buf << "@ #{rec['season']['currentMatchday']}"
22
+ buf << "\n"
23
+
24
+ buf
25
+ end
26
+
27
+ def self.fmt_match( rec )
28
+ buf = String.new
29
+
30
+ ## -- todo - make sure / assert it's always utc - how???
31
+ ## utc = ## tz_utc.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' )
32
+ ## note: DateTime.strptime is supposed to be unaware of timezones!!!
33
+ ## use to parse utc
34
+ utc = DateTime.strptime( rec['utcDate'], '%Y-%m-%dT%H:%M:%SZ' ).to_time.utc
35
+ assert( utc.strftime( '%Y-%m-%dT%H:%M:%SZ' ) == rec['utcDate'], 'utc time mismatch' )
36
+
37
+ status = rec['status']
38
+ assert( %w[SCHEDULED
39
+ TIMED
40
+ FINISHED
41
+ POSTPONED
42
+ IN_PLAY
43
+ ].include?( status ), "unknown status - #{status}" )
44
+
45
+ buf << '%-10s' % status
46
+ buf << utc.strftime( '%a %b %d %Y %H:%M')
47
+ buf << ' '
48
+ # pp rec['utcDate']
49
+
50
+ team1 = rec['homeTeam']['name'] ?
51
+ "#{rec['homeTeam']['name']} (#{rec['homeTeam']['tla']})" : '?'
52
+ team2 = rec['awayTeam']['name'] ?
53
+ "#{rec['awayTeam']['name']} (#{rec['awayTeam']['tla']})" : '?'
54
+ buf << '%22s' % team1
55
+ buf << " - "
56
+ buf << '%-22s' % team2
57
+ buf << " "
58
+
59
+ stage = rec['stage']
60
+ group = rec['group']
61
+
62
+ buf << "#{rec['matchday']} - #{stage} "
63
+ buf << "/ #{group} " if group
64
+ buf << "\n"
65
+
66
+ buf << " "
67
+ buf << '%-20s' % rec['score']['duration']
68
+ buf << ' '*24
69
+
70
+ duration = rec['score']['duration']
71
+ assert( %w[REGULAR
72
+ EXTRA_TIME
73
+ PENALTY_SHOOTOUT
74
+ ].include?( duration ), "unknown duration - #{duration}" )
75
+
76
+ score = String.new
77
+
78
+ if duration == 'PENALTY_SHOOTOUT'
79
+ if rec['score']['extraTime']
80
+ ## quick & dirty hack - calc et via regulartime+extratime
81
+ score << "#{rec['score']['penalties']['home']}-#{rec['score']['penalties']['away']} pen. "
82
+ score << "#{rec['score']['regularTime']['home']+rec['score']['extraTime']['home']}"
83
+ score << "-"
84
+ score << "#{rec['score']['regularTime']['away']+rec['score']['extraTime']['away']}"
85
+ score << " a.e.t. "
86
+ score << "(#{rec['score']['regularTime']['home']}-#{rec['score']['regularTime']['away']},"
87
+ score << "#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
88
+ else ### south american-style (no extra time)
89
+ ## quick & dirty hacke - calc ft via fullTime-penalties
90
+ score << "#{rec['score']['penalties']['home']}-#{rec['score']['penalties']['away']} pen. "
91
+ score << "(#{rec['score']['fullTime']['home']-rec['score']['penalties']['home']}"
92
+ score << "-"
93
+ score << "#{rec['score']['fullTime']['away']-rec['score']['penalties']['away']},"
94
+ score << "#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
95
+ end
96
+ elsif duration == 'EXTRA_TIME'
97
+ score << "#{rec['score']['regularTime']['home']+rec['score']['extraTime']['home']}"
98
+ score << "-"
99
+ score << "#{rec['score']['regularTime']['away']+rec['score']['extraTime']['away']}"
100
+ score << " a.e.t. "
101
+ score << "(#{rec['score']['regularTime']['home']}-#{rec['score']['regularTime']['away']},"
102
+ score << "#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
103
+ elsif duration == 'REGULAR'
104
+ if rec['score']['fullTime']['home'] && rec['score']['fullTime']['away']
105
+ score << "#{rec['score']['fullTime']['home']}-#{rec['score']['fullTime']['away']} "
106
+ score << "(#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
107
+ end
108
+ else
109
+ raise ArgumentError, "unexpected/unknown score duration #{rec['score']['duration']}"
110
+ end
111
+
112
+
113
+ buf << score
114
+ buf << "\n"
115
+ buf
116
+ end
117
+
118
+
119
+ def self.pp_matches( data )
120
+
121
+ ## track match status and score duration
122
+ stats = { 'status' => Hash.new(0),
123
+ 'duration' => Hash.new(0),
124
+ 'stage' => Hash.new(0),
125
+ 'group' => Hash.new(0),
126
+ }
127
+
128
+ first = Date.strptime( data['resultSet']['first'], '%Y-%m-%d' )
129
+ last = Date.strptime( data['resultSet']['last'], '%Y-%m-%d' )
130
+
131
+ diff = (last - first).to_i # note - returns rational number (e.g. 30/1)
132
+
133
+
134
+ print "==> #{data['competition']['name']}, "
135
+ print "#{first.strftime('%a %b %d %Y')} - #{last.strftime('%a %b %d %Y')}"
136
+ print " (#{diff}d)"
137
+ print "\n"
138
+
139
+ data['matches'].each do |rec|
140
+
141
+ print fmt_match( rec )
142
+
143
+ ## track stats
144
+ status = rec['status']
145
+ stats['status'][status] += 1
146
+
147
+ stage = rec['stage']
148
+ stats['stage'][stage] += 1
149
+
150
+ group = rec['group']
151
+ stats['group'][group] += 1 if group
152
+
153
+ duration = rec['score']['duration']
154
+ stats['duration'][duration] += 1
155
+ end
156
+
157
+ print " #{data['resultSet']['played']}/#{data['resultSet']['count']} matches"
158
+ print "\n"
159
+
160
+ print " status (#{stats['status'].size}): "
161
+ print fmt_count( stats['status'], sort: true )
162
+ print "\n"
163
+ print " duration (#{stats['duration'].size}): "
164
+ print fmt_count( stats['duration'], sort: true )
165
+ print "\n"
166
+ print " stage (#{stats['stage'].size}): "
167
+ print fmt_count( stats['stage'] )
168
+ print "\n"
169
+ print " group (#{stats['group'].size}): "
170
+ print fmt_count( stats['group'] )
171
+ print "\n"
172
+ end
173
+
174
+
175
+ def self.fmt_count( h, sort: false )
176
+ pairs = h.to_a
177
+ if sort
178
+ pairs = pairs.sort do |l,r|
179
+ res = r[1] <=> l[1] ## bigger number first
180
+ res = l[0] <=> r[0] if res == 0
181
+ res
182
+ end
183
+ end
184
+ pairs = pairs.map { |name,count| "#{name} (#{count})" }
185
+ pairs.join( ' · ' )
186
+ end
187
+
188
+
189
+ end # module Footballdata
@@ -0,0 +1,59 @@
1
+ module Footballdata
2
+
3
+ class Stat ## rename to match stat or something why? why not?
4
+ def initialize
5
+ @data = {}
6
+ end
7
+
8
+ def [](key) @data[ key ]; end
9
+
10
+ def update( match )
11
+ ## keep track of some statistics
12
+ stat = @data[:all] ||= { stage: Hash.new( 0 ),
13
+ duration: Hash.new( 0 ),
14
+ status: Hash.new( 0 ),
15
+ group: Hash.new( 0 ),
16
+ matchday: Hash.new( 0 ),
17
+
18
+ matches: 0,
19
+ goals: 0,
20
+ }
21
+
22
+ stat[:stage][ match['stage'] ] += 1
23
+ stat[:group][ match['group'] ] += 1
24
+ stat[:status][ match['status'] ] += 1
25
+ stat[:matchday][ match['matchday'] ] += 1
26
+
27
+ score = match['score']
28
+
29
+ stat[:duration][ score['duration'] ] += 1 ## track - assert always REGULAR
30
+
31
+ stat[:matches] += 1
32
+ stat[:goals] += score['fullTime']['homeTeam'].to_i if score['fullTime']['homeTeam']
33
+ stat[:goals] += score['fullTime']['awayTeam'].to_i if score['fullTime']['awayTeam']
34
+
35
+
36
+ stage_key = match['stage'].downcase.to_sym # e.g. :regular_season
37
+ stat = @data[ stage_key ] ||= { duration: Hash.new( 0 ),
38
+ status: Hash.new( 0 ),
39
+ group: Hash.new( 0 ),
40
+ matchday: Hash.new( 0 ),
41
+
42
+ matches: 0,
43
+ goals: 0,
44
+ }
45
+ stat[:group][ match['group'] ] += 1
46
+ stat[:status][ match['status'] ] += 1
47
+ stat[:matchday][ match['matchday'] ] += 1
48
+
49
+ stat[:duration][ score['duration'] ] += 1 ## track - assert always REGULAR
50
+
51
+ stat[:matches] += 1
52
+ stat[:goals] += score['fullTime']['homeTeam'].to_i if score['fullTime']['homeTeam']
53
+ stat[:goals] += score['fullTime']['awayTeam'].to_i if score['fullTime']['awayTeam']
54
+ end
55
+ end # class Stat
56
+ end # module Footballdata
57
+
58
+
59
+
@@ -0,0 +1,90 @@
1
+
2
+ ###########
3
+ ### export teams
4
+
5
+
6
+ module Footballdata
7
+
8
+ def self.export_teams( league:, season: )
9
+
10
+ season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
11
+ league_code = LEAGUES[league.downcase]
12
+
13
+ teams_url = Metal.competition_teams_url( league_code,
14
+ season.start_year )
15
+ data_teams = Webcache.read_json( teams_url )
16
+
17
+ ## build a (reverse) team lookup by name
18
+ puts "#{data_teams['teams'].size} teams"
19
+
20
+
21
+
22
+ clubs = {} ## by country
23
+
24
+ data_teams['teams'].each do |rec|
25
+
26
+ buf = String.new ### use for string buffer (or String.new('') - why? why not?
27
+ buf << "#{rec['name']}"
28
+ buf << ", #{rec['founded']}" if rec['founded']
29
+ if rec['venue']
30
+ buf << ", @ #{rec['venue']}"
31
+ # buf << " # #{rec['area']['name']}"
32
+ end
33
+ buf << "\n"
34
+
35
+ alt_names = String.new
36
+ alt_names << " | #{rec['shortName']}" if rec['shortName'] &&
37
+ rec['shortName'] != rec['name'] &&
38
+ rec['shortName'] != rec['tla']
39
+
40
+ alt_names << " | #{rec['tla']}" if rec['tla'] &&
41
+ rec['tla'] != rec['name']
42
+
43
+ if alt_names.size > 0
44
+ buf << " "
45
+ buf << alt_names
46
+ buf << "\n"
47
+ end
48
+
49
+ ## clean null in address (or keep nulls) - why? why not?
50
+ ## e.g. null Rionegro null
51
+ ## Calle 104 No. 13a - 32 Bogotá null
52
+
53
+ buf << " address: #{rec['address'].gsub( /\bnull\b/, '')}"
54
+ buf << "\n"
55
+ buf << " web: #{rec['website']}"
56
+ buf << "\n"
57
+ buf << " colors: #{rec['clubColors']}"
58
+ buf << "\n"
59
+
60
+ country = rec['area']['name']
61
+ ary = clubs[ country] ||= []
62
+ ary << buf
63
+ end
64
+ # puts buf
65
+
66
+ ## pp clubs
67
+
68
+
69
+ buf = String.new
70
+
71
+ if clubs.size > 1
72
+ clubs.each do |country, ary|
73
+ buf << "# #{country} - #{ary.size} clubs\n"
74
+ end
75
+ buf << "\n"
76
+ end
77
+
78
+ clubs.each do |country, ary|
79
+ buf << "= #{country} # #{ary.size} clubs\n\n"
80
+ buf << ary.join( "\n" )
81
+ buf << "\n\n"
82
+ end
83
+
84
+ path = "#{config.convert.out_dir}/#{season.to_path}/#{league.downcase}.clubs.txt"
85
+ write_text( path, buf )
86
+ end
87
+
88
+
89
+ end # module Footballdata
90
+
@@ -0,0 +1,20 @@
1
+
2
+ module FootballdataApi
3
+ MAJOR = 0 ## todo: namespace inside version or something - why? why not??
4
+ MINOR = 2
5
+ PATCH = 0
6
+ VERSION = [MAJOR,MINOR,PATCH].join('.')
7
+
8
+ def self.version
9
+ VERSION
10
+ end
11
+
12
+ def self.banner
13
+ "footballdata-api/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
14
+ end
15
+
16
+ def self.root
17
+ File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
18
+ end
19
+ end # module FootballdataApi
20
+
@@ -0,0 +1,56 @@
1
+ ## 3rd party (our own)
2
+ require 'season/formats' ## add season support
3
+ require 'webget' ## incl. webget, webcache, webclient, etc.
4
+
5
+
6
+ require 'cocos' ## check if webget incl. cocos ??
7
+
8
+
9
+ require 'tzinfo'
10
+
11
+
12
+ module Footballdata
13
+ class Configuration
14
+ #########
15
+ ## nested configuration classes - use - why? why not?
16
+ class Convert
17
+ def out_dir() @out_dir || './o'; end
18
+ def out_dir=(value) @out_dir = value; end
19
+ end
20
+
21
+ def convert() @convert ||= Convert.new; end
22
+ end # class Configuration
23
+
24
+ ## lets you use
25
+ ## Footballdata.configure do |config|
26
+ ## config.convert.out_dir = './o'
27
+ ## end
28
+ def self.configure() yield( config ); end
29
+ def self.config() @config ||= Configuration.new; end
30
+ end # module Footballdata
31
+
32
+
33
+
34
+
35
+ ###
36
+ # our own code
37
+ require_relative 'footballdata/version'
38
+ require_relative 'footballdata/leagues'
39
+ require_relative 'footballdata/download'
40
+ require_relative 'footballdata/prettyprint'
41
+
42
+ require_relative 'footballdata/mods'
43
+ require_relative 'footballdata/convert'
44
+ require_relative 'footballdata/teams'
45
+
46
+
47
+ require_relative 'footballdata/generator'
48
+
49
+
50
+
51
+ ### for processing tool
52
+ ## (auto-)add sportdb/writer (pulls in sportdb/catalogs and gitti)
53
+ ## require 'sportdb/writers'
54
+
55
+
56
+ puts FootballdataApi.banner ## say hello
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: footballdata-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Gerald Bauer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tzinfo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
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: season-formats
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webget
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: cocos
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rdoc
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '4.0'
76
+ - - "<"
77
+ - !ruby/object:Gem::Version
78
+ version: '7'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '4.0'
86
+ - - "<"
87
+ - !ruby/object:Gem::Version
88
+ version: '7'
89
+ - !ruby/object:Gem::Dependency
90
+ name: hoe
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '4.1'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '4.1'
103
+ description: footballdata-api - get football data via Daniel Freitag's football-data.org
104
+ api v4
105
+ email: gerald.bauer@gmail.com
106
+ executables:
107
+ - fbdat
108
+ extensions: []
109
+ extra_rdoc_files:
110
+ - CHANGELOG.md
111
+ - Manifest.txt
112
+ - README.md
113
+ files:
114
+ - CHANGELOG.md
115
+ - Manifest.txt
116
+ - README.md
117
+ - Rakefile
118
+ - bin/fbdat
119
+ - lib/footballdata.rb
120
+ - lib/footballdata/convert.rb
121
+ - lib/footballdata/download.rb
122
+ - lib/footballdata/generator.rb
123
+ - lib/footballdata/leagues.rb
124
+ - lib/footballdata/mods.rb
125
+ - lib/footballdata/prettyprint.rb
126
+ - lib/footballdata/stat.rb
127
+ - lib/footballdata/teams.rb
128
+ - lib/footballdata/version.rb
129
+ homepage: https://github.com/sportdb/sport.db
130
+ licenses:
131
+ - Public Domain
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options:
135
+ - "--main"
136
+ - README.md
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: 2.2.2
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubygems_version: 3.4.10
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: footballdata-api - get football data via Daniel Freitag's football-data.org
154
+ api v4
155
+ test_files: []