sportdb-writers 0.1.1 → 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,211 @@
1
+
2
+ module Fbgen
3
+ def self.main( args=ARGV )
4
+
5
+ opts = {
6
+ source_path: [],
7
+ push: false,
8
+ dry: false, ## dry run (no write)
9
+ debug: true,
10
+ file: nil,
11
+ }
12
+
13
+ parser = OptionParser.new do |parser|
14
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options] [args]"
15
+
16
+ parser.on( "-p", "--push",
17
+ "fast forward sync and commit & push changes to git repo - default is (#{opts[:push]})" ) do |push|
18
+ opts[:push] = push
19
+ end
20
+ parser.on( "--dry",
21
+ "dry run; do NOT write - default is (#{opts[:dry]})" ) do |dry|
22
+ opts[:dry] = dry
23
+ end
24
+ parser.on( "-q", "--quiet",
25
+ "less debug output/messages - default is (#{!opts[:debug]})" ) do |debug|
26
+ opts[:debug] = !debug
27
+ end
28
+
29
+ parser.on( "-f FILE", "--file FILE",
30
+ "read leagues via .csv file") do |file|
31
+ opts[:file] = file
32
+ end
33
+ end
34
+ parser.parse!( args )
35
+
36
+
37
+
38
+ if opts[:source_path].empty? &&
39
+ File.exist?( '/sports/cache.api.fbdat') &&
40
+ File.exist?( '/sports/cache.wfb' )
41
+ opts[:source_path] << '/sports/cache.api.fbdat'
42
+ opts[:source_path] << '/sports/cache.wfb'
43
+ end
44
+
45
+
46
+ puts "OPTS:"
47
+ p opts
48
+ puts "ARGV:"
49
+ p args
50
+
51
+ datasets = if opts[:file]
52
+ read_datasets( opts[:file] )
53
+ else
54
+ parse_datasets( args )
55
+ end
56
+
57
+ puts "datasets:"
58
+ pp datasets
59
+
60
+
61
+ source_path = opts[:source_path]
62
+ source_path = ['.'] if source_path.empty? ## use ./ as default
63
+
64
+ root_dir = if opts[:push]
65
+ GitHubSync.root # e.g. "/sports"
66
+ else
67
+ './o'
68
+ end
69
+
70
+ puts " (output) root_dir: >#{root_dir}<"
71
+
72
+ sync = if opts[:push]
73
+ repos = GitHubSync.find_repos( datasets )
74
+ puts " #{repos.size} repo(s):"
75
+ pp repos
76
+ GitHubSync.new( repos )
77
+ else
78
+ nil
79
+ end
80
+ puts " sync:"
81
+ pp sync
82
+
83
+ sync.git_fast_forward_if_clean if sync
84
+
85
+
86
+
87
+ datasets.each do |league_key, seasons|
88
+ seasons = [ Season('2024/25') ] if seasons.empty?
89
+
90
+ puts "==> gen #{league_key} - #{seasons.size} seasons(s)..."
91
+
92
+ league_info = Writer::LEAGUES[ league_key ]
93
+ pp league_info
94
+
95
+ seasons.each do |season|
96
+ ### get matches
97
+
98
+ filename = "#{season.to_path}/#{league_key}.csv"
99
+ path = find_file( filename, path: source_path )
100
+
101
+ if path.nil?
102
+ puts "!! no source found for #{filename}; sorry"
103
+ exit 1
104
+ end
105
+
106
+ puts " ---> reading matches in #{path} ..."
107
+ matches = SportDb::CsvMatchParser.read( path )
108
+ puts " #{matches.size} matches"
109
+
110
+ ## build
111
+ txt = SportDb::TxtMatchWriter.build( matches )
112
+ puts txt if opts[:debug]
113
+
114
+ league_name = league_info[ :name ] # e.g. Brasileiro Série A
115
+ basename = league_info[ :basename] #.e.g 1-seriea
116
+
117
+ league_name = league_name.call( season ) if league_name.is_a?( Proc ) ## is proc/func - name depends on season
118
+ basename = basename.call( season ) if basename.is_a?( Proc ) ## is proc/func - name depends on season
119
+
120
+ buf = String.new
121
+ buf << "= #{league_name} #{season}\n\n"
122
+ buf << txt
123
+
124
+ repo = GitHubSync::REPOS[ league_key ]
125
+ repo_path = "#{repo['owner']}/#{repo['name']}"
126
+ repo_path << "/#{repo['path']}" if repo['path'] ## note: do NOT forget to add optional extra path!!!
127
+
128
+ outpath = "#{root_dir}/#{repo_path}/#{season.to_path}/#{basename}.txt"
129
+ if opts[:dry]
130
+ puts " (dry) writing to >#{outpath}<..."
131
+ else
132
+ write_text( outpath, buf )
133
+ end
134
+ end
135
+ end
136
+
137
+ sync.git_push_if_changes if sync
138
+
139
+ end # method self.main
140
+
141
+
142
+
143
+ def self.parse_datasets( args )
144
+ ### split args in datasets with leagues and seasons
145
+ datasets = []
146
+ args.each do |arg|
147
+ if arg =~ %r{^[0-9/-]+$} ## season
148
+ if datasets.empty?
149
+ puts "!! ERROR - league required before season arg; sorry"
150
+ exit 1
151
+ end
152
+
153
+ season = Season.parse( arg ) ## check season
154
+ datasets[-1][1] << season
155
+ else ## assume league key
156
+ key = arg.downcase
157
+ league_info = Writer::LEAGUES[ key ]
158
+
159
+ if league_info.nil?
160
+ puts "!! ERROR - no league found for >#{key}<; sorry"
161
+ exit 1
162
+ end
163
+
164
+ datasets << [key, []]
165
+ end
166
+ end
167
+ datasets
168
+ end
169
+
170
+
171
+ def self.read_datasets( path )
172
+ ### split args in datasets with leagues and seasons
173
+ datasets = []
174
+ recs = read_csv( path )
175
+ recs.each do |rec|
176
+ league_code = rec['league']
177
+ key = league_code.downcase
178
+ league_info = Writer::LEAGUES[ key ]
179
+
180
+ if league_info.nil?
181
+ puts "!! ERROR - no league found for >#{key}<; sorry"
182
+ exit 1
183
+ end
184
+
185
+ datasets << [key, []]
186
+
187
+ seasons_str = rec['seasons']
188
+ seasons = seasons_str.split( /[ ]+/ )
189
+
190
+ seasons.each do |season_str|
191
+ season = Season.parse( season_str ) ## check season
192
+ datasets[-1][1] << season
193
+ end
194
+ end
195
+ datasets
196
+ end
197
+
198
+
199
+ def self.find_file( filename, path: )
200
+ path.each do |src_dir|
201
+ path = "#{src_dir}/#{filename}"
202
+ return path if File.exist?( path )
203
+ end
204
+
205
+ ## fix - raise file not found error!!!
206
+ nil ## not found - raise filenot found error - why? why not?
207
+ end
208
+
209
+
210
+
211
+ end # module Fbgen
@@ -0,0 +1,37 @@
1
+
2
+ module Fbtxt
3
+ def self.main( args=ARGV )
4
+
5
+
6
+ opts = {
7
+ }
8
+
9
+ parser = OptionParser.new do |parser|
10
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options]"
11
+ end
12
+ parser.parse!( args )
13
+
14
+ puts "OPTS:"
15
+ p opts
16
+ puts "ARGV:"
17
+ p args
18
+
19
+
20
+ matches = []
21
+
22
+ ## step 1 - get all matches via csv
23
+ args.each do |arg|
24
+ path = arg
25
+ puts "==> reading matches in #{path} ..."
26
+ more_matches = SportDb::CsvMatchParser.read( path )
27
+ matches += more_matches
28
+ end
29
+
30
+ puts "#{matches.size} matches"
31
+ puts
32
+
33
+ txt = SportDb::TxtMatchWriter.build( matches )
34
+ puts txt
35
+ puts
36
+ end
37
+ end # module Fbtxt
@@ -0,0 +1,83 @@
1
+
2
+ module SportDb
3
+ class LeagueConfig
4
+
5
+ def self.read( path )
6
+ recs = read_csv( path )
7
+ new( recs )
8
+ end
9
+
10
+ def initialize( recs=nil )
11
+ @table = {}
12
+ add( recs ) if recs
13
+ end
14
+
15
+
16
+ class LeagueItem
17
+ def initialize
18
+ @recs = []
19
+ end
20
+ def add( rec ) @recs << rec; end
21
+ alias_method :<<, :add
22
+
23
+ def find_by_season( season )
24
+ @recs.each do |rec|
25
+ start_season = rec['start_season']
26
+ end_season = rec['end_season']
27
+ return rec if (start_season.nil? || start_season <= season) &&
28
+ (end_season.nil? || end_season >= season)
29
+ end
30
+ nil
31
+ end
32
+
33
+
34
+ def name_by_season( season )
35
+ rec = find_by_season( season )
36
+ rec ? rec['name'] : nil
37
+ end
38
+
39
+ def basename_by_season( season )
40
+ rec = find_by_season( season )
41
+ rec ? rec['basename'] : nil
42
+ end
43
+
44
+
45
+ def [](key)
46
+ ## short cut - if only one or zero rec
47
+ ## return directly
48
+ if @recs.empty?
49
+ nil
50
+ elsif @recs.size == 1 &&
51
+ @recs[0]['start_season'].nil? &&
52
+ @recs[0]['end_season'].nil?
53
+ @recs[0][key.to_s]
54
+ else ### return proc that requires season arg
55
+ case key.to_sym
56
+ when :name then method(:name_by_season).to_proc
57
+ when :basename then method(:basename_by_season).to_proc
58
+ else
59
+ nil ## return nil - why? why not?
60
+ ## raise ArgumentError, "invalid key #{key}; use :name or :basename"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def add( recs )
67
+ recs.each do |rec|
68
+ @table[ rec['key'] ] ||= LeagueItem.new
69
+
70
+ ## note: auto-change seasons to season object or nil
71
+ @table[ rec['key'] ] << { 'name' => rec['name'],
72
+ 'basename' => rec['basename'],
73
+ 'start_season' => rec['start_season'].empty? ? nil : Season.parse( rec['start_season'] ),
74
+ 'end_season' => rec['end_season'].empty? ? nil : Season.parse( rec['end_season'] ),
75
+ }
76
+ end
77
+ end
78
+
79
+
80
+ def [](key) @table[ key.to_s.downcase ]; end
81
+
82
+ end # class LeagueConfig
83
+ end # module SportDb
@@ -19,12 +19,116 @@ def self.build( matches, rounds: true )
19
19
  ## note: make sure rounds is a bool, that is, true or false (do NOT pass in strings etc.)
20
20
  raise ArgumentError, "rounds flag - bool expected; got: #{rounds.inspect}" unless rounds.is_a?( TrueClass ) || rounds.is_a?( FalseClass )
21
21
 
22
- ## note: for now always english
22
+
23
+ ### check for stages & stats
24
+ stats = { 'stage' => Hash.new(0),
25
+ 'date' => { 'start_date' => nil,
26
+ 'end_date' => nil, },
27
+ 'teams' => Hash.new(0),
28
+ }
29
+
30
+ ## add matches played stats too??
31
+
32
+ matches.each do |match|
33
+ stage = match.stage
34
+ stage = 'Regular Season' if stage.nil? || stage.empty?
35
+ stats['stage'][ stage ] += 1
36
+
37
+ if match.date
38
+
39
+ ## todo/fix - norm date (parse as Date)
40
+ ## check format etc.
41
+ date = if match.date.is_a?( String )
42
+ Date.strptime( match.date, '%Y-%m-%d' )
43
+ else ## assume it's already a date (object)
44
+ match.date
45
+ end
46
+ stats['date']['start_date'] ||= date
47
+ stats['date']['end_date'] ||= date
48
+
49
+ stats['date']['start_date'] = date if date < stats['date']['start_date']
50
+ stats['date']['end_date'] = date if date > stats['date']['end_date']
51
+ end
52
+
53
+ [match.team1, match.team2].each do |team|
54
+ stats['teams'][ team ] += 1 if team && !['N.N.'].include?( team )
55
+ end
56
+ end
57
+
58
+ use_stages = if stats['stage'].size >= 2 ||
59
+ (stats['stage'].size == 1 &&
60
+ stats['stage'].keys[0] != 'Regular Season')
61
+ true
62
+ else
63
+ false
64
+ end
65
+
66
+
67
+ ### add comment header
68
+ buf = String.new
69
+ # e.g. 13 April – 25 September 2024
70
+ # or 16 August 2024 – 25 May 2025
71
+ buf << "# Date "
72
+ start_date = stats['date']['start_date']
73
+ end_date = stats['date']['end_date']
74
+ if start_date.year != end_date.year
75
+ buf << "#{start_date.strftime('%a %b/%-d %Y')} - #{end_date.strftime('%a %b/%-d %Y')}"
76
+ else
77
+ buf << "#{start_date.strftime('%a %b/%-d')} - #{end_date.strftime('%a %b/%-d %Y')}"
78
+ end
79
+ buf << " (#{end_date.jd-start_date.jd}d)" ## add days
80
+ buf << "\n"
81
+
82
+ buf << "# Teams #{stats['teams'].size}\n"
83
+ buf << "# Matches #{matches.size}\n"
84
+
85
+ if use_stages
86
+ buf << "# Stages "
87
+ stages = stats['stage'].map { |name,count| "#{name} (#{count})" }.join( ' ' )
88
+ buf << stages
89
+ buf << "\n"
90
+ end
91
+ buf << "\n\n"
92
+
93
+
94
+ if use_stages
95
+ ## split matches by stage
96
+ matches_by_stage = {}
97
+ matches.each do |match|
98
+ stage = match.stage || ''
99
+ matches_by_stage[stage] ||= []
100
+ matches_by_stage[stage] << match
101
+ end
102
+
103
+ ## todo/fix
104
+ ## note - empty stage must go first!!!!
105
+ matches_by_stage.each_with_index do |(name, matches),i|
106
+ buf << "\n" if i != 0 # add extra new line (if not first stage)
107
+ if name.empty?
108
+ buf << "# Regular Season\n" ## empty stage
109
+ else
110
+ buf << "== #{name}\n"
111
+ end
112
+ buf += _build_batch( matches, rounds: rounds )
113
+ buf << "\n" if i+1 != matches_by_stage.size
114
+ end
115
+ buf
116
+ else
117
+ buf += _build_batch( matches, rounds: rounds )
118
+ buf
119
+ end
120
+ end
121
+
122
+
123
+ def self._build_batch( matches, rounds: true )
124
+ ## note: make sure rounds is a bool, that is, true or false (do NOT pass in strings etc.)
125
+ raise ArgumentError, "rounds flag - bool expected; got: #{rounds.inspect}" unless rounds.is_a?( TrueClass ) || rounds.is_a?( FalseClass )
126
+
127
+ ## note: for now always english
23
128
  round = 'Matchday'
24
129
  format_date = ->(date) { date.strftime( '%a %b/%-d' ) }
25
130
  format_score = ->(match) { match.score.to_s( lang: 'en' ) }
26
131
  round_translations = ROUND_TRANSLATIONS
27
-
28
132
 
29
133
  buf = String.new
30
134
 
@@ -33,12 +137,12 @@ def self.build( matches, rounds: true )
33
137
  last_time = nil
34
138
 
35
139
 
36
- matches.each do |match|
140
+ matches.each_with_index do |match,i|
37
141
 
38
142
  ## note: make rounds optional (set rounds flag to false to turn off)
39
143
  if rounds
40
144
  if match.round != last_round
41
- buf << "\n\n"
145
+ buf << (i == 0 ? "\n" : "\n\n") ## start with single empty line
42
146
  if match.round.is_a?( Integer ) ||
43
147
  match.round =~ /^[0-9]+$/ ## all numbers/digits
44
148
  ## default "class format
@@ -3,8 +3,8 @@ module SportDb
3
3
  module Module
4
4
  module Writers
5
5
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
6
- MINOR = 1
7
- PATCH = 1
6
+ MINOR = 2
7
+ PATCH = 0
8
8
  VERSION = [MAJOR,MINOR,PATCH].join('.')
9
9
 
10
10
  def self.version
@@ -53,7 +53,7 @@ def self.write( league:, season:,
53
53
  source:,
54
54
  extra: nil,
55
55
  split: false,
56
- normalize: false,
56
+ normalize: false,
57
57
  rounds: true )
58
58
  season = Season( season ) ## normalize season
59
59
 
@@ -71,7 +71,7 @@ def self.write( league:, season:,
71
71
  exit 1
72
72
  end
73
73
  source_info = { path: source } ## wrap in "plain" source dir in source info
74
-
74
+
75
75
  source_path = source_info[:path]
76
76
 
77
77
  ## format lets you specify directory layout
@@ -106,14 +106,14 @@ def self.write( league:, season:,
106
106
  if normalize.is_a?(Proc)
107
107
  matches = normalize.call( matches, league: league,
108
108
  season: season )
109
- else
109
+ else
110
110
  puts "!! ERROR - normalize; expected proc got #{normalize.inspect}"
111
111
  exit 1
112
- end
112
+ end
113
113
  end
114
-
115
114
 
116
-
115
+
116
+
117
117
  league_name = league_info[ :name ] # e.g. Brasileiro Série A
118
118
  basename = league_info[ :basename] #.e.g 1-seriea
119
119
 
@@ -122,7 +122,10 @@ def self.write( league:, season:,
122
122
 
123
123
  ## note - repo_path moved!!!
124
124
  ## repo_path = league_info[ :path ] # e.g. brazil or world/europe/portugal etc.
125
- repo_path = SportDb::GitHubSync::REPOS[ league ]
125
+ repo = SportDb::GitHubSync::REPOS[ league ]
126
+ repo_path = "#{repo['owner']}/#{repo['name']}"
127
+ repo_path << "/#{repo['path']}" if repo['path'] ## note: do NOT forget to add optional extra path!!!
128
+
126
129
 
127
130
 
128
131
  season_path = String.new ## note: allow extra path for output!!!! e.g. archive/2000s etc.
@@ -175,7 +178,7 @@ def self.write( league:, season:,
175
178
  )
176
179
 
177
180
  ## note: might be empty!!! if no matches skip (do NOT write)
178
- write_text( "#{config.out_dir}/#{repo_path}/#{season_path}/#{stage_basename}.txt",
181
+ write_text( "#{config.out_dir}/#{repo_path}/#{season_path}/#{stage_basename}.txt",
179
182
  buf ) unless buf.empty?
180
183
  end
181
184
  else ## no stages - assume "regular" plain vanilla season
@@ -2,6 +2,9 @@
2
2
  require 'sportdb/quick'
3
3
 
4
4
 
5
+ require 'optparse' ## command-line processing; check if included updstream?
6
+
7
+
5
8
 
6
9
  module Writer
7
10
  class Configuration
@@ -29,30 +32,74 @@ require_relative 'writers/goals'
29
32
  require_relative 'writers/write'
30
33
 
31
34
 
35
+ ## setup leagues (info) table
36
+ require_relative 'writers/league_config'
37
+
38
+ module Writer
39
+ LEAGUES = SportDb::LeagueConfig.new
40
+
41
+ ['leagues_europe',
42
+ 'leagues_america',
43
+ 'leagues_world'
44
+ ].each do |name|
45
+ recs = read_csv( "#{SportDb::Module::Writers.root}/config/#{name}.csv" )
46
+ LEAGUES.add( recs )
47
+ end
48
+ end # module Writer
49
+
50
+
51
+ ###
52
+ # fbtxt tool
53
+ require_relative 'fbtxt/main'
54
+
55
+
32
56
 
33
57
  ########################
34
58
  # push & pull github scripts
35
59
  require 'gitti' ## note - requires git machinery
36
60
 
37
- require_relative 'writers/github' ## github helpers/update machinery
61
+ require_relative 'fbgen/github_config'
62
+ require_relative 'fbgen/github' ## github helpers/update machinery
38
63
 
39
64
 
65
+ module Fbgen
66
+ class GitHubSync
67
+ REPOS = GitHubConfig.new
68
+ recs = read_csv( "#{SportDb::Module::Writers.root}/config/openfootball.csv" )
69
+ REPOS.add( recs )
40
70
 
41
- ## setup empty leagues (info) hash
42
- module Writer
43
- LEAGUES = {}
71
+ ## todo/check: find a better name for helper?
72
+ ## note: datasets of format
73
+ ##
74
+ ## DATASETS = [
75
+ ## ['it.1', %w[2020/21 2019/20]],
76
+ ## ['it.2', %w[2019/20]],
77
+ ## ['es.1', %w[2019/20]],
78
+ ## ['es.2', %w[2019/20]],
79
+ ## ]
80
+
81
+ def self.find_repos( datasets )
82
+ repos = []
83
+ datasets.each do |dataset|
84
+ league_key = dataset[0]
85
+ repo = REPOS[ league_key ]
86
+ ## pp repo
87
+ if repo.nil?
88
+ puts "!! ERROR - no repo config/path found for league >#{league_key}<; sorry"
89
+ exit 1
90
+ end
91
+
92
+ repos << "#{repo['owner']}/#{repo['name']}"
93
+ end
94
+
95
+ pp repos
96
+ repos.uniq ## note: remove duplicates (e.g. europe or world or such)
44
97
  end
98
+ end # class GitHubSync
99
+ end # module Fbgen
45
100
 
46
- require_relative 'leagues/leagues_at'
47
- require_relative 'leagues/leagues_de'
48
- require_relative 'leagues/leagues_eng'
49
- require_relative 'leagues/leagues_es'
50
- require_relative 'leagues/leagues_europe'
51
- require_relative 'leagues/leagues_it'
52
- require_relative 'leagues/leagues_mx'
53
- require_relative 'leagues/leagues_south_america'
54
- require_relative 'leagues/leagues_world'
55
101
 
102
+ require_relative 'fbgen/main'
56
103
 
57
104
 
58
105
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sportdb-writers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
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-09-04 00:00:00.000000000 Z
11
+ date: 2024-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sportdb-quick
@@ -90,18 +90,17 @@ files:
90
90
  - Rakefile
91
91
  - bin/fbgen
92
92
  - bin/fbtxt
93
- - lib/sportdb/leagues/leagues_at.rb
94
- - lib/sportdb/leagues/leagues_de.rb
95
- - lib/sportdb/leagues/leagues_eng.rb
96
- - lib/sportdb/leagues/leagues_es.rb
97
- - lib/sportdb/leagues/leagues_europe.rb
98
- - lib/sportdb/leagues/leagues_it.rb
99
- - lib/sportdb/leagues/leagues_mx.rb
100
- - lib/sportdb/leagues/leagues_south_america.rb
101
- - lib/sportdb/leagues/leagues_world.rb
93
+ - config/leagues_america.csv
94
+ - config/leagues_europe.csv
95
+ - config/leagues_world.csv
96
+ - config/openfootball.csv
97
+ - lib/sportdb/fbgen/github.rb
98
+ - lib/sportdb/fbgen/github_config.rb
99
+ - lib/sportdb/fbgen/main.rb
100
+ - lib/sportdb/fbtxt/main.rb
102
101
  - lib/sportdb/writers.rb
103
- - lib/sportdb/writers/github.rb
104
102
  - lib/sportdb/writers/goals.rb
103
+ - lib/sportdb/writers/league_config.rb
105
104
  - lib/sportdb/writers/txt_writer.rb
106
105
  - lib/sportdb/writers/version.rb
107
106
  - lib/sportdb/writers/write.rb