footballdata-api 0.2.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/Manifest.txt +3 -1
- data/Rakefile +3 -3
- data/bin/fbdat +56 -34
- data/config/leagues.csv +54 -0
- data/config/timezones.csv +27 -0
- data/lib/footballdata/convert.rb +263 -100
- data/lib/footballdata/download.rb +11 -11
- data/lib/footballdata/leagues.rb +20 -55
- data/lib/footballdata/mods.rb +2 -0
- data/lib/footballdata/prettyprint.rb +29 -47
- data/lib/footballdata/teams.rb +13 -14
- data/lib/footballdata/timezones.rb +97 -0
- data/lib/footballdata/version.rb +2 -2
- data/lib/footballdata.rb +3 -8
- metadata +6 -4
- data/lib/footballdata/generator.rb +0 -33
@@ -17,7 +17,7 @@ def self.fmt_competition( rec )
|
|
17
17
|
buf << "#{rec['competition']['name']} (#{rec['competition']['code']}) -- "
|
18
18
|
buf << "#{rec['area']['name']} (#{rec['area']['code']}) "
|
19
19
|
buf << "#{rec['competition']['type']} "
|
20
|
-
buf << "#{rec['season']['startDate']} - #{rec['season']['endDate']} "
|
20
|
+
buf << "#{rec['season']['startDate']} - #{rec['season']['endDate']} "
|
21
21
|
buf << "@ #{rec['season']['currentMatchday']}"
|
22
22
|
buf << "\n"
|
23
23
|
|
@@ -26,12 +26,12 @@ end
|
|
26
26
|
|
27
27
|
def self.fmt_match( rec )
|
28
28
|
buf = String.new
|
29
|
-
|
29
|
+
|
30
30
|
## -- todo - make sure / assert it's always utc - how???
|
31
31
|
## utc = ## tz_utc.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' )
|
32
32
|
## note: DateTime.strptime is supposed to be unaware of timezones!!!
|
33
33
|
## use to parse utc
|
34
|
-
utc = DateTime.strptime( rec['utcDate'], '%Y-%m-%dT%H:%M:%SZ' ).to_time.utc
|
34
|
+
utc = DateTime.strptime( rec['utcDate'], '%Y-%m-%dT%H:%M:%SZ' ).to_time.utc
|
35
35
|
assert( utc.strftime( '%Y-%m-%dT%H:%M:%SZ' ) == rec['utcDate'], 'utc time mismatch' )
|
36
36
|
|
37
37
|
status = rec['status']
|
@@ -50,7 +50,7 @@ def self.fmt_match( rec )
|
|
50
50
|
team1 = rec['homeTeam']['name'] ?
|
51
51
|
"#{rec['homeTeam']['name']} (#{rec['homeTeam']['tla']})" : '?'
|
52
52
|
team2 = rec['awayTeam']['name'] ?
|
53
|
-
"#{rec['awayTeam']['name']} (#{rec['awayTeam']['tla']})" : '?'
|
53
|
+
"#{rec['awayTeam']['name']} (#{rec['awayTeam']['tla']})" : '?'
|
54
54
|
buf << '%22s' % team1
|
55
55
|
buf << " - "
|
56
56
|
buf << '%-22s' % team2
|
@@ -58,7 +58,7 @@ def self.fmt_match( rec )
|
|
58
58
|
|
59
59
|
stage = rec['stage']
|
60
60
|
group = rec['group']
|
61
|
-
|
61
|
+
|
62
62
|
buf << "#{rec['matchday']} - #{stage} "
|
63
63
|
buf << "/ #{group} " if group
|
64
64
|
buf << "\n"
|
@@ -66,49 +66,31 @@ def self.fmt_match( rec )
|
|
66
66
|
buf << " "
|
67
67
|
buf << '%-20s' % rec['score']['duration']
|
68
68
|
buf << ' '*24
|
69
|
-
|
69
|
+
|
70
70
|
duration = rec['score']['duration']
|
71
71
|
assert( %w[REGULAR
|
72
72
|
EXTRA_TIME
|
73
73
|
PENALTY_SHOOTOUT
|
74
74
|
].include?( duration ), "unknown duration - #{duration}" )
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
76
|
+
|
77
|
+
ft, ht, et, pen = convert_score( rec['score'] )
|
78
|
+
score = String.new
|
79
|
+
if !pen.empty?
|
80
|
+
if et.empty? ### south american-style (no extra time)
|
81
|
+
score << "#{pen} pen. "
|
82
|
+
score << "(#{ft}, #{ht})"
|
83
|
+
else
|
84
|
+
score << "#{pen} pen. "
|
85
|
+
score << "#{et} a.e.t. "
|
86
|
+
score << "(#{ft}, #{ht})"
|
87
|
+
end
|
88
|
+
elsif !et.empty?
|
89
|
+
score << "#{et} a.e.t. "
|
90
|
+
score << "(#{ft}, #{ht})"
|
108
91
|
else
|
109
|
-
|
92
|
+
score << "#{ft} (#{ht})"
|
110
93
|
end
|
111
|
-
|
112
94
|
|
113
95
|
buf << score
|
114
96
|
buf << "\n"
|
@@ -118,14 +100,14 @@ end
|
|
118
100
|
|
119
101
|
def self.pp_matches( data )
|
120
102
|
|
121
|
-
## track match status and score duration
|
103
|
+
## track match status and score duration
|
122
104
|
stats = { 'status' => Hash.new(0),
|
123
105
|
'duration' => Hash.new(0),
|
124
106
|
'stage' => Hash.new(0),
|
125
107
|
'group' => Hash.new(0),
|
126
108
|
}
|
127
109
|
|
128
|
-
first = Date.strptime( data['resultSet']['first'], '%Y-%m-%d' )
|
110
|
+
first = Date.strptime( data['resultSet']['first'], '%Y-%m-%d' )
|
129
111
|
last = Date.strptime( data['resultSet']['last'], '%Y-%m-%d' )
|
130
112
|
|
131
113
|
diff = (last - first).to_i # note - returns rational number (e.g. 30/1)
|
@@ -142,16 +124,16 @@ def self.pp_matches( data )
|
|
142
124
|
|
143
125
|
## track stats
|
144
126
|
status = rec['status']
|
145
|
-
stats['status'][status] += 1
|
127
|
+
stats['status'][status] += 1
|
146
128
|
|
147
129
|
stage = rec['stage']
|
148
|
-
stats['stage'][stage] += 1
|
130
|
+
stats['stage'][stage] += 1
|
149
131
|
|
150
132
|
group = rec['group']
|
151
|
-
stats['group'][group] += 1 if group
|
133
|
+
stats['group'][group] += 1 if group
|
152
134
|
|
153
135
|
duration = rec['score']['duration']
|
154
|
-
stats['duration'][duration] += 1
|
136
|
+
stats['duration'][duration] += 1
|
155
137
|
end
|
156
138
|
|
157
139
|
print " #{data['resultSet']['played']}/#{data['resultSet']['count']} matches"
|
@@ -182,7 +164,7 @@ def self.fmt_count( h, sort: false )
|
|
182
164
|
end
|
183
165
|
end
|
184
166
|
pairs = pairs.map { |name,count| "#{name} (#{count})" }
|
185
|
-
pairs.join( ' · ' )
|
167
|
+
pairs.join( ' · ' )
|
186
168
|
end
|
187
169
|
|
188
170
|
|
data/lib/footballdata/teams.rb
CHANGED
@@ -8,9 +8,9 @@ module Footballdata
|
|
8
8
|
def self.export_teams( league:, season: )
|
9
9
|
|
10
10
|
season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
|
11
|
-
league_code =
|
11
|
+
league_code = find_league!( league )
|
12
12
|
|
13
|
-
teams_url = Metal.competition_teams_url( league_code,
|
13
|
+
teams_url = Metal.competition_teams_url( league_code,
|
14
14
|
season.start_year )
|
15
15
|
data_teams = Webcache.read_json( teams_url )
|
16
16
|
|
@@ -22,12 +22,12 @@ def self.export_teams( league:, season: )
|
|
22
22
|
clubs = {} ## by country
|
23
23
|
|
24
24
|
data_teams['teams'].each do |rec|
|
25
|
-
|
26
|
-
buf = String.new ### use for string buffer (or String.new('') - why? why not?
|
25
|
+
|
26
|
+
buf = String.new ### use for string buffer (or String.new('') - why? why not?
|
27
27
|
buf << "#{rec['name']}"
|
28
28
|
buf << ", #{rec['founded']}" if rec['founded']
|
29
|
-
if rec['venue']
|
30
|
-
buf << ", @ #{rec['venue']}"
|
29
|
+
if rec['venue']
|
30
|
+
buf << ", @ #{rec['venue']}"
|
31
31
|
# buf << " # #{rec['area']['name']}"
|
32
32
|
end
|
33
33
|
buf << "\n"
|
@@ -39,7 +39,7 @@ def self.export_teams( league:, season: )
|
|
39
39
|
|
40
40
|
alt_names << " | #{rec['tla']}" if rec['tla'] &&
|
41
41
|
rec['tla'] != rec['name']
|
42
|
-
|
42
|
+
|
43
43
|
if alt_names.size > 0
|
44
44
|
buf << " "
|
45
45
|
buf << alt_names
|
@@ -48,7 +48,7 @@ def self.export_teams( league:, season: )
|
|
48
48
|
|
49
49
|
## clean null in address (or keep nulls) - why? why not?
|
50
50
|
## e.g. null Rionegro null
|
51
|
-
## Calle 104 No. 13a - 32 Bogotá null
|
51
|
+
## Calle 104 No. 13a - 32 Bogotá null
|
52
52
|
|
53
53
|
buf << " address: #{rec['address'].gsub( /\bnull\b/, '')}"
|
54
54
|
buf << "\n"
|
@@ -57,21 +57,21 @@ def self.export_teams( league:, season: )
|
|
57
57
|
buf << " colors: #{rec['clubColors']}"
|
58
58
|
buf << "\n"
|
59
59
|
|
60
|
-
country = rec['area']['name']
|
60
|
+
country = rec['area']['name']
|
61
61
|
ary = clubs[ country] ||= []
|
62
62
|
ary << buf
|
63
63
|
end
|
64
64
|
# puts buf
|
65
|
-
|
65
|
+
|
66
66
|
## pp clubs
|
67
67
|
|
68
|
-
|
68
|
+
|
69
69
|
buf = String.new
|
70
70
|
|
71
71
|
if clubs.size > 1
|
72
72
|
clubs.each do |country, ary|
|
73
73
|
buf << "# #{country} - #{ary.size} clubs\n"
|
74
|
-
end
|
74
|
+
end
|
75
75
|
buf << "\n"
|
76
76
|
end
|
77
77
|
|
@@ -82,9 +82,8 @@ def self.export_teams( league:, season: )
|
|
82
82
|
end
|
83
83
|
|
84
84
|
path = "#{config.convert.out_dir}/#{season.to_path}/#{league.downcase}.clubs.txt"
|
85
|
-
write_text( path, buf )
|
85
|
+
write_text( path, buf )
|
86
86
|
end
|
87
87
|
|
88
88
|
|
89
89
|
end # module Footballdata
|
90
|
-
|
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
class UTC
|
3
|
+
def self.now() Time.now.utc; end
|
4
|
+
def self.today() now.to_date; end
|
5
|
+
|
6
|
+
## -- todo - make sure / assert it's always utc - how???
|
7
|
+
## utc = ## tz_utc.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' )
|
8
|
+
## note: DateTime.strptime is supposed to be unaware of timezones!!!
|
9
|
+
## use to parse utc
|
10
|
+
## quick hack -
|
11
|
+
## use to_time.getutc instead of utc ???
|
12
|
+
def self.strptime( str, format )
|
13
|
+
DateTime.strptime( str, format ).to_time.utc
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find_zone( name )
|
17
|
+
zone = TZInfo::Timezone.get( name )
|
18
|
+
## wrap tzinfo timezone in our own - for adding more (auto)checks etc.
|
19
|
+
zone ? Timezone.new( zone ) : nil
|
20
|
+
end
|
21
|
+
|
22
|
+
class Timezone ## nested inside UTC
|
23
|
+
## todo/fix
|
24
|
+
## cache timezone - why? why not?
|
25
|
+
def initialize( zone )
|
26
|
+
@zone = zone
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_local( time )
|
30
|
+
## assert time is Time (not Date or DateTIme)
|
31
|
+
## and assert utc!!!
|
32
|
+
assert( time.is_a?( Time ), "time #{time} is NOT of class Time; got #{time.class.name}" )
|
33
|
+
assert( time.utc?, "time #{time} is NOT utc; utc? returns #{time.utc?}" )
|
34
|
+
local = @zone.to_local( time )
|
35
|
+
local
|
36
|
+
end
|
37
|
+
|
38
|
+
def assert( cond, msg )
|
39
|
+
if cond
|
40
|
+
# do nothing
|
41
|
+
else
|
42
|
+
puts "!!! assert failed - #{msg}"
|
43
|
+
exit 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end # class Timezone
|
47
|
+
end # class UTC
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
module Footballdata
|
52
|
+
def self.find_zone!( league:, season: )
|
53
|
+
@zones ||= begin
|
54
|
+
recs = read_csv( "#{FootballdataApi.root}/config/timezones.csv" )
|
55
|
+
zones = {}
|
56
|
+
recs.each do |rec|
|
57
|
+
zone = UTC.find_zone( rec['zone'] )
|
58
|
+
if zone.nil?
|
59
|
+
## raise ArgumentError - invalid zone
|
60
|
+
puts "!! ERROR - cannot find timezone in timezone db:"
|
61
|
+
pp rec
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
zones[ rec['key']] = zone
|
65
|
+
end
|
66
|
+
zones
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
## lookup first try by league+season
|
71
|
+
league_code = league.downcase
|
72
|
+
season = Season( season )
|
73
|
+
|
74
|
+
## e.g. world+2022, etc.
|
75
|
+
key = "#{league_code}+#{season}"
|
76
|
+
zone = @zones[key]
|
77
|
+
|
78
|
+
## try league e.g. eng.1 etc.
|
79
|
+
zone = @zones[league_code] if zone.nil?
|
80
|
+
|
81
|
+
## try first code only (country code )
|
82
|
+
if zone.nil?
|
83
|
+
code, _ = league_code.split( '.', 2 )
|
84
|
+
zone = @zones[code]
|
85
|
+
end
|
86
|
+
|
87
|
+
if zone.nil? ## still not found; report error
|
88
|
+
puts "!! ERROR: no timezone found for #{league} #{season}"
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
|
92
|
+
zone
|
93
|
+
end
|
94
|
+
end # module Footballdata
|
95
|
+
|
96
|
+
|
97
|
+
|
data/lib/footballdata/version.rb
CHANGED
data/lib/footballdata.rb
CHANGED
@@ -17,7 +17,7 @@ module Footballdata
|
|
17
17
|
def out_dir() @out_dir || './o'; end
|
18
18
|
def out_dir=(value) @out_dir = value; end
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
def convert() @convert ||= Convert.new; end
|
22
22
|
end # class Configuration
|
23
23
|
|
@@ -36,6 +36,8 @@ end # module Footballdata
|
|
36
36
|
# our own code
|
37
37
|
require_relative 'footballdata/version'
|
38
38
|
require_relative 'footballdata/leagues'
|
39
|
+
require_relative 'footballdata/timezones'
|
40
|
+
|
39
41
|
require_relative 'footballdata/download'
|
40
42
|
require_relative 'footballdata/prettyprint'
|
41
43
|
|
@@ -44,13 +46,6 @@ require_relative 'footballdata/convert'
|
|
44
46
|
require_relative 'footballdata/teams'
|
45
47
|
|
46
48
|
|
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
49
|
|
55
50
|
|
56
51
|
puts FootballdataApi.banner ## say hello
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: footballdata-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tzinfo
|
@@ -116,15 +116,17 @@ files:
|
|
116
116
|
- README.md
|
117
117
|
- Rakefile
|
118
118
|
- bin/fbdat
|
119
|
+
- config/leagues.csv
|
120
|
+
- config/timezones.csv
|
119
121
|
- lib/footballdata.rb
|
120
122
|
- lib/footballdata/convert.rb
|
121
123
|
- lib/footballdata/download.rb
|
122
|
-
- lib/footballdata/generator.rb
|
123
124
|
- lib/footballdata/leagues.rb
|
124
125
|
- lib/footballdata/mods.rb
|
125
126
|
- lib/footballdata/prettyprint.rb
|
126
127
|
- lib/footballdata/stat.rb
|
127
128
|
- lib/footballdata/teams.rb
|
129
|
+
- lib/footballdata/timezones.rb
|
128
130
|
- lib/footballdata/version.rb
|
129
131
|
homepage: https://github.com/sportdb/sport.db
|
130
132
|
licenses:
|
@@ -140,7 +142,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
140
142
|
requirements:
|
141
143
|
- - ">="
|
142
144
|
- !ruby/object:Gem::Version
|
143
|
-
version:
|
145
|
+
version: 3.1.0
|
144
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
147
|
requirements:
|
146
148
|
- - ">="
|
@@ -1,33 +0,0 @@
|
|
1
|
-
|
2
|
-
##
|
3
|
-
### check - change Generator to Writer
|
4
|
-
## and write( league:, season: ) - why? why not?
|
5
|
-
|
6
|
-
module Footballdata
|
7
|
-
class Generator
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
###########
|
12
|
-
## always download for now - why? why not?
|
13
|
-
## support cache - why? why not?
|
14
|
-
def generate( league:, season: )
|
15
|
-
|
16
|
-
## for testing use cached version always - why? why not?
|
17
|
-
## step 1 - download
|
18
|
-
Footballdata.schedule( league: league, season: season )
|
19
|
-
|
20
|
-
## step 2 - convert (to .csv)
|
21
|
-
|
22
|
-
## todo/fix - convert in-memory and return matches
|
23
|
-
Footballdata.convert( league: league, season: season )
|
24
|
-
|
25
|
-
source_dir = Footballdata.config.convert.out_dir
|
26
|
-
|
27
|
-
Writer.write( league: league,
|
28
|
-
season: season,
|
29
|
-
source: source_dir )
|
30
|
-
end # def generate
|
31
|
-
|
32
|
-
end # class Generator
|
33
|
-
end # module Footballdata
|