aoc_cli 0.1.3 → 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.
@@ -1,15 +1,20 @@
1
1
  module AocCli
2
2
  module Database
3
+ def self.correct(attempt:)
4
+ attempt = Attempt.new(attempt:attempt).correct
5
+ Stats::Complete.new(n:attempt.count_attempts).update
6
+ Calendar::Part.new.increment
7
+ end
3
8
  class Query
4
9
  require 'sqlite3'
5
10
  attr_reader :db
6
11
  def initialize(path:)
7
12
  @db = SQLite3::Database.open(path)
8
13
  end
9
- def select(t:, cols:"*", data:)
14
+ def select(t:, cols:"*", where:)
10
15
  db.execute(
11
16
  "SELECT #{cols} FROM #{t} "\
12
- "WHERE #{data.map{|k, v| "#{k} = #{v}"}.join(" AND ")}")
17
+ "WHERE #{where.map{|k, v| "#{k} = #{v}"}.join(" AND ")}")
13
18
  end
14
19
  def table(t:, cols:)
15
20
  db.execute(
@@ -31,25 +36,24 @@ module AocCli
31
36
  self
32
37
  end
33
38
  end
34
- class Log
39
+ class Attempt
35
40
  attr_reader :attempt, :db
36
41
  def initialize(attempt:)
37
42
  @attempt = attempt
38
- @db = Query.new(path:Paths::Database
39
- .cfg("#{attempt.user}"))
43
+ @db = Query.new(path:Paths::Database.cfg(attempt.user))
40
44
  .table(t:"attempts", cols:cols)
41
45
  end
42
46
  def correct
43
47
  db.insert(t:"attempts", val:data << 1 << 0 << 0)
44
48
  self
45
49
  end
46
- def incorrect(high:, low:)
47
- db.insert(t:"attempts", val:data.push(0)
48
- .push(parse_hint(high:high, low:low)))
50
+ def incorrect(low:, high:)
51
+ db.insert(t:"attempts",
52
+ val:data << 0 << parse_hint(low:low, high:high))
49
53
  self
50
54
  end
51
- def parse_hint(high:, low:)
52
- [ high ? 1 : 0, low ? 1 : 0 ]
55
+ def parse_hint(low:, high:)
56
+ [ low ? 1 : 0, high ? 1 : 0 ]
53
57
  end
54
58
  def data
55
59
  [ "'#{Time.now}'",
@@ -69,7 +73,7 @@ module AocCli
69
73
  high: :INT }
70
74
  end
71
75
  def count_attempts
72
- db.select(t:"attempts", data:where).count
76
+ db.select(t:"attempts", where:where).count
73
77
  end
74
78
  def where
75
79
  { year:attempt.year,
@@ -148,12 +152,69 @@ module AocCli
148
152
  .map{|t| t.to_i.to_s.rjust(2, "0")}.join(":")
149
153
  end
150
154
  def dl_time
151
- @dl_time ||= Time
152
- .parse(db
153
- .select(t:"stats", cols:"dl_time", data:where)
155
+ @dl_time ||= Time.parse(db
156
+ .select(t:"stats", cols:"dl_time", where:where)
154
157
  .flatten.first)
155
158
  end
156
159
  end
157
160
  end
161
+ module Calendar
162
+ class Init
163
+ attr_reader :user, :year, :db, :stars
164
+ def initialize(u:Metafile.get(:user),
165
+ y:Metafile.get(:year),
166
+ stars:)
167
+ @user = Validate.user(u)
168
+ @year = Validate.year(y)
169
+ @stars = stars
170
+ @db = Query
171
+ .new(path:Paths::Database.cfg(user))
172
+ .table(t:"calendar", cols:cols)
173
+ end
174
+ def cols
175
+ { year: :INT,
176
+ day: :INT,
177
+ stars: :TEXT }
178
+ end
179
+ def n_stars(day)
180
+ stars.keys.include?(day) ? stars[day] : 0
181
+ end
182
+ def day_data(day)
183
+ ["'#{year}'", "'#{day}'", "'#{n_stars(day)}'"]
184
+ end
185
+ def table_exist?
186
+ db.select(t:"calendar", where:{year:"'#{year}'"}).count > 0
187
+ end
188
+ def insert
189
+ unless table_exist?
190
+ 1.upto(25){|day|
191
+ db.insert(t:"calendar",
192
+ val:day_data(day))}
193
+ end
194
+ end
195
+ end
196
+ class Part
197
+ attr_reader :user, :year, :day, :db
198
+ def initialize(u:Metafile.get(:user),
199
+ y:Metafile.get(:year),
200
+ d:Metafile.get(:day))
201
+ @user = Validate.user(u)
202
+ @year = Validate.year(y)
203
+ @day = Validate.day(d)
204
+ @db = Query.new(path:Paths::Database.cfg(user))
205
+ end
206
+ def get
207
+ db.select(t:"calendar", cols:"stars", where:where)
208
+ .flatten.first.to_i + 1
209
+ end
210
+ def increment
211
+ db.update(t:"calendar", val:{stars:get}, where:where)
212
+ end
213
+ def where
214
+ { year:"'#{year}'",
215
+ day:"'#{day}'" }
216
+ end
217
+ end
218
+ end
158
219
  end
159
220
  end
data/lib/aoc_cli/day.rb CHANGED
@@ -1,12 +1,7 @@
1
1
  module AocCli
2
2
  module Day
3
- def self.refresh
4
- puts "- Updating puzzle...".yellow
5
- Init.new.write
6
- Data::Puzzle.new.write
7
- end
8
3
  class Init
9
- attr_reader :year, :day, :user, :paths, :part
4
+ attr_reader :user, :year, :day, :paths
10
5
  def initialize(u:Metafile.get(:user),
11
6
  y:Metafile.get(:year),
12
7
  d:Metafile.get(:day))
@@ -19,135 +14,111 @@ module AocCli
19
14
  Dir.mkdir(Validate.day_dir(paths.day_dir))
20
15
  self
21
16
  end
22
- def write
17
+ def meta
23
18
  File.write(paths.local(f:"meta"),
24
- Metafile.day(u:user, y:year, d:day))
19
+ Metafile.day(u:user, y:year, d:day))
25
20
  self
26
21
  end
27
22
  end
28
23
  class Pages < Init
29
- attr_reader :cache, :files
24
+ attr_reader :files, :use_cache
30
25
  def initialize(u:Metafile.get(:user),
31
26
  y:Metafile.get(:year),
32
27
  d:Metafile.get(:day),
33
- f:[:Input, :Puzzle])
28
+ f:[:Input, :Puzzle],
29
+ use_cache:true)
34
30
  super(u:u, y:y, d:d)
35
31
  @files = f
32
+ @use_cache = use_cache
36
33
  end
37
- def write
38
- cache.each{|page, data| data ?
39
- File.write(paths.local(f:page), data) :
40
- download(page:page)}
34
+ def load
35
+ files.each do |file| use_cache && cache[file] ?
36
+ copy(file:file) : download(page:file) end
41
37
  end
42
- private
43
38
  def cache
44
- @cache ||= Cache
45
- .new(u:user, y:year, d:day, f:files).load
39
+ @cache ||= Cache.new(d:day, f:files).query
40
+ end
41
+ def copy(file:)
42
+ File.write(paths.local(f:file), cache[file])
46
43
  end
47
- def download(page:, to:paths.cache_and_local(f:page))
48
- dl = Object.const_get("AocCli::Day::Data::#{page}")
44
+ def download(page:)
45
+ req = Requests.const_get(page)
49
46
  .new(u:user, y:year, d:day)
50
- .write(to:to)
51
- dl.init_db if dl.class
52
- .instance_methods
53
- .include?(:init_db)
47
+ .write(to:paths.cache_and_local(f:page))
48
+ req.init_stats if page == :Puzzle
49
+ end
50
+ end
51
+ class Cache < Pages
52
+ def initialize(u:Metafile.get(:user),
53
+ y:Metafile.get(:year),
54
+ d:Metafile.get(:day),
55
+ f:[:Input, :Puzzle])
56
+ super(u:u, y:y, d:d, f:f)
57
+ paths.create_cache
58
+ end
59
+ def query
60
+ files.map{|file| [file, read(file:file)]}.to_h
61
+ end
62
+ private
63
+ def read(file:)
64
+ File.exist?(paths.cache_path(f:file)) ?
65
+ File.read(paths.cache_path(f:file)) : nil
54
66
  end
55
67
  end
56
- module Data
57
- class DayObject < Init
58
- attr_reader :user, :year, :day, :data, :paths, :part
68
+ module Requests
69
+ class Request < Init
70
+ attr_reader :data, :part
59
71
  def initialize(u:Metafile.get(:user),
60
72
  y:Metafile.get(:year),
61
73
  d:Metafile.get(:day))
62
74
  super(u:u, y:y, d:d)
75
+ @data = parse(raw:fetch)
63
76
  @part = Metafile.part(d:day)
64
- @data = parse(raw: fetch)
65
77
  end
66
- def write(to:paths.cache_and_local(f:page))
67
- to.each{|path| File.write(path, data)}
68
- self
78
+ def write(to:)
79
+ to.each{|path| File.write(path, data)}; self
69
80
  end
70
81
  private
71
82
  def fetch
72
83
  Tools::Get.new(u:user, y:year, d:day, p:page)
73
84
  end
74
85
  end
75
- class Puzzle < DayObject
86
+ class Puzzle < Request
76
87
  def page
77
88
  :Puzzle
78
89
  end
79
- def parse(raw:)
90
+ def parse(raw:fetch)
80
91
  raw.chunk(f:"<article", t:"<\/article", f_off:2)
81
92
  .md
82
93
  .gsub(/(?<=\])\[\]/, "")
83
94
  .gsub(/\n.*<!--.*-->.*\n/, "")
84
95
  end
85
- def init_db
96
+ def init_stats
86
97
  Database::Stats::Init
87
98
  .new(d:day, p:part)
88
- .init if part < 3
89
- self
99
+ .init if part < 3 && !stats_exist?
100
+ end
101
+ def stats_exist?
102
+ Database::Query
103
+ .new(path:Paths::Database.cfg(user))
104
+ .select(t:"stats",
105
+ where:{year:year, day:day, part:part})
106
+ .count > 0
90
107
  end
91
108
  end
92
- class Input < DayObject
109
+ class Input < Request
93
110
  def page
94
111
  :Input
95
112
  end
96
- def parse(raw:)
113
+ def parse(raw:fetch)
97
114
  raw.raw
98
115
  end
99
116
  end
100
117
  end
101
- class Cache < Pages
102
- require 'fileutils'
103
- def initialize(u:Metafile.get(:user),
104
- y:Metafile.get(:year),
105
- d:Metafile.get(:day),
106
- f:[:Input, :Puzzle])
107
- super(u:u, y:y, d:d, f:f)
108
- FileUtils.mkdir_p(paths.cache_dir) unless Dir
109
- .exist?(paths.cache_dir)
110
- end
111
- def load
112
- files.map{|f| [f, read_file(f:f)]}.to_h
113
- end
114
- private
115
- def read_file(f:)
116
- cached?(f:f) ? read(f:f) : nil
117
- end
118
- def cached?(f:)
119
- File.exist?(paths.cache_path(f:f))
120
- end
121
- def read(f:)
122
- File.read(paths.cache_path(f:f))
123
- end
124
- end
125
- class Reddit
126
- attr_reader :year, :day, :uniq, :browser
127
- def initialize(y:Metafile.get(:year),
128
- d:Metafile.get(:day),
129
- b:false)
130
- @year = Validate.year(y)
131
- @day = Validate.day(d)
132
- @uniq = Database::Query
133
- .new(path:Paths::Database.root("reddit"))
134
- .select(t:"'#{year}'", data:{day:"'#{day}'"})
135
- .flatten[1]
136
- @browser = b
137
- end
138
- def open
139
- system("#{browser ? "open" : cmd} #{link}")
140
- end
141
- def cmd
142
- ["ttrv", "rtv"]
143
- .map{|cli| cli unless `which #{cli}`.empty?}
144
- .reject{|cmd| cmd.nil?}&.first || "open"
145
- end
146
- def link
147
- "https://www.reddit.com/r/"\
148
- "adventofcode/comments/#{uniq}/"\
149
- "#{year}_day_#{day}_solutions"
150
- end
118
+ def self.refresh(files:[:Input, :Puzzle])
119
+ puts "- Updating puzzle...".blue
120
+ Init.new.meta
121
+ Pages.new(f:files, use_cache:false).load
151
122
  end
152
123
  end
153
124
  end
@@ -112,7 +112,7 @@ module AocCli
112
112
  def message
113
113
  <<~error
114
114
  #{ERROR}: No session key value.
115
- Use the #{"-k".yellow} or #{"--key".yellow} flags
115
+ Use the #{"-k".yellow} or #{"--key".yellow} flags to store the key
116
116
  error
117
117
  end
118
118
  end
@@ -123,7 +123,7 @@ module AocCli
123
123
  end
124
124
  def message
125
125
  <<~error
126
- #{ERROR}: The key #{user.yellow} already exists in your config file
126
+ #{ERROR}: The key #{key.yellow} already exists in your config file
127
127
  error
128
128
  end
129
129
  end
@@ -160,12 +160,27 @@ module AocCli
160
160
  error
161
161
  end
162
162
  end
163
- class NoCmd < StandardError
163
+ class CmdNil < StandardError
164
164
  def message
165
165
  <<~error
166
166
  #{ERROR}: Flags passed but no command specified
167
167
  error
168
168
  end
169
169
  end
170
+ class KeyInv < StandardError
171
+ def message
172
+ <<~error
173
+ #{ERROR}: Invalid key
174
+ Double check your session key. It should start with "session="
175
+ error
176
+ end
177
+ end
178
+ class ConfigExist < StandardError
179
+ def message
180
+ <<~error
181
+ #{ERROR}: A config file already exists in #{Paths::Config.path.blue}
182
+ error
183
+ end
184
+ end
170
185
  end
171
186
  end
data/lib/aoc_cli/files.rb CHANGED
@@ -1,52 +1,106 @@
1
1
  module AocCli
2
2
  module Files
3
- class Config
4
- require 'fileutils'
5
- def initialize
6
- Paths::Config.create
7
- end
8
- def def_acc
9
- get_line(key:"default") || "main"
10
- end
11
- def is_set?(key:nil, val:nil)
12
- read.split("\n").grep(/#{key}=>#{val}/).any?
13
- end
14
- def mod_line(key:, val:)
15
- is_set?(key:key) ?
16
- write(f:read.gsub(/(?<=^#{key}=>).*$/,
17
- val.to_s)) :
3
+ module Config
4
+ class Tools
5
+ def self.is_set?(key:nil, val:nil)
6
+ read.split("\n").grep(/(?<!\/\/)#{key}=>#{val}/).any?
7
+ end
8
+ def self.get_all(key:)
9
+ read.scan(/(?:(?<=(?<!\/\/)#{key}=>)).*$/)
10
+ end
11
+ def self.get_line(key:)
12
+ get_all(key:key)&.first
13
+ #read.scan(/(?:(?<=(?<!\/\/)#{key}=>)).*$/)&.first
14
+ end
15
+ def self.get_bool(key:)
16
+ get_line(key:key) == "true" ? true : false
17
+ end
18
+ def self.mod_line(key:, val:)
19
+ is_set?(key:key) ?
20
+ write(f:read.gsub(/(?<=^#{key}=>).*$/, val.to_s)) :
21
+ write(f:"#{key}=>#{val}\n", m:"a")
22
+ end
23
+ def self.set_line(key:, val:)
18
24
  write(f:"#{key}=>#{val}\n", m:"a")
19
- end
20
- def get_line(key:)
21
- read.scan(/(?<=#{key}=>).*$/)&.first
22
- end
23
- def get_bool(key:)
24
- get_line(key:key) == "true" ? true : false
25
- end
26
- protected
27
- def set_line(key:, val:)
28
- write(f:"#{key}=>#{val}\n", m:"a")
29
- end
30
- private
31
- def read
32
- File.read(Paths::Config.path)
33
- end
34
- def write(f:, m:"w")
35
- File.write(Paths::Config.path, f, mode:m)
36
- end
37
- end
38
- class Cookie < Config
39
- attr_reader :user
40
- def initialize(u:)
41
- @user = u
42
- super()
43
- end
44
- def store(key:)
45
- set_line(key:"cookie=>#{Validate.set_user(user)}",
46
- val:Validate.set_key(key))
47
- end
48
- def key
49
- get_line(key:"cookie=>#{user}")
25
+ end
26
+ private
27
+ def self.read
28
+ Paths::Config.create
29
+ File.read(Paths::Config.path)
30
+ end
31
+ def self.write(f:, m:"w")
32
+ Paths::Config.create
33
+ File.write(Paths::Config.path, f, mode:m)
34
+ end
35
+ end
36
+ class Prefs < Tools
37
+ def self.default_alias
38
+ is_set?(key:"default") ? get_line(key:"default") :
39
+ is_set?(key:"cookie=>main") ? "main" :
40
+ list_aliases.first || "main"
41
+ end
42
+ def self.list_aliases
43
+ get_all(key:"cookie")&.map{|a| a.gsub(/=>.*/, "")}
44
+ end
45
+ def self.bool(key:)
46
+ is_set?(key:key) ?
47
+ get_bool(key:key) : defaults[key.to_sym]
48
+ end
49
+ def self.string(key:)
50
+ is_set?(key:key) ?
51
+ get_line(key:key) : defaults[key.to_sym]
52
+ end
53
+ private
54
+ def self.defaults
55
+ { calendar_file:true,
56
+ ignore_md_files:true,
57
+ ignore_meta_files:true,
58
+ init_git:false,
59
+ lb_in_calendar:true,
60
+ reddit_in_browser:false,
61
+ unicode_tables:true
62
+ }
63
+ end
64
+ end
65
+ class Cookie < Tools
66
+ def self.store(user:, key:)
67
+ set_line(key:"cookie=>#{Validate.set_user(user)}",
68
+ val:Validate.set_key(key))
69
+ end
70
+ def self.key(user:)
71
+ Validate.key(get_line(key:"cookie=>#{Validate
72
+ .user(user)}"))
73
+ end
74
+ end
75
+ class Example
76
+ def self.write
77
+ File.write(Validate.no_config, file)
78
+ end
79
+ def self.file
80
+ <<~file
81
+ //aoc-cli example config
82
+ //See the github repo for more information on configuring aoc-cli
83
+ //https://github.com/apexatoll/aoc-cli
84
+
85
+ [General]
86
+ //Print table in unicode rather than ascii
87
+ unicode_tables=>true
88
+ //Open Reddit in browser rather than use a Reddit CLI
89
+ reddit_in_browser=>false
90
+
91
+ [Initialise Year]
92
+ //Create a calendar file
93
+ calendar_file=>true
94
+ //Initialise git repo on year initialisation
95
+ init_git=>false
96
+ //Add calendar and puzzle files to gitignore
97
+ ignore_md_files=>true
98
+ //Add .meta files to gitignore
99
+ ignore_meta_files=>true
100
+ //Include leaderboard stats in calendar file
101
+ lb_in_calendar=>true
102
+ file
103
+ end
50
104
  end
51
105
  end
52
106
  class Metafile
@@ -56,22 +110,13 @@ module AocCli
56
110
  def self.type
57
111
  get("dir").to_sym
58
112
  end
59
- def self.add(hash:, path:".meta")
60
- hash.map {|k, v| "#{k}=>#{v}\n"}
61
- .each{|l| File.write(path, l, mode:"a")}
62
- end
63
113
  def self.part(d:)
64
- JSON.parse(read(dir:root_dir)
65
- .scan(/(?<=stars=>).*$/)&.first)[d.to_s]
66
- .to_i + 1
114
+ Database::Calendar::Part.new(d:d).get
67
115
  end
68
116
  private
69
117
  def self.read(dir:".")
70
118
  File.read("#{Validate.init(dir)}/.meta")
71
119
  end
72
- def self.root_dir
73
- type == :ROOT ? "." : ".."
74
- end
75
120
  def self.year(u:, y:)
76
121
  <<~meta
77
122
  dir=>ROOT
@@ -89,5 +134,29 @@ module AocCli
89
134
  meta
90
135
  end
91
136
  end
137
+ class Calendar
138
+ attr_reader :cal, :stats, :year
139
+ def initialize(y:Metafile.get(:year), cal:, stats:)
140
+ @year, @cal, @stats = Validate.year(y), cal, stats
141
+ end
142
+ def include_leaderboard?
143
+ Prefs.bool(key:"lb_in_calendar")
144
+ end
145
+ def title
146
+ "Year #{year}: #{stats.total_stars}/50 *"
147
+ end
148
+ def underline
149
+ "-" * (cal.data[0].to_s.length + 2)
150
+ end
151
+ def make
152
+ <<~file
153
+ #{title}
154
+ #{underline}
155
+ #{cal.data.join("\n")}\n
156
+ #{stats.data.join("\n") if stats.total_stars > 0 &&
157
+ include_leaderboard?}
158
+ file
159
+ end
160
+ end
92
161
  end
93
162
  end