footballdata-api 0.2.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,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: []