berklee-valencia 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 652e951a9218cd0b5c2c2090e548b0e87280b29f2e69e6fc294666b424682cac
4
+ data.tar.gz: 8a5972d1d46680b7a4a7b2a87253fe13e3d4713b3715de9b0c7faff286dc35fd
5
+ SHA512:
6
+ metadata.gz: 9a89957b9d1fd8d9fc65e28410eafc0a5e3e61e69310b89aa4a8dd199ed93df2a0e95d7498c3db970617880239af660c0759d68f3fc0fc83955ee9cb43800145
7
+ data.tar.gz: 224ec32ca96b0ff2951b0995c3ab8dd9d519b772dd0764c26f5cf54ae39a59ec895f389046404882016f6eaf19261e0f8530a7b7aec66539520a8cb81e82aac9
@@ -0,0 +1,22 @@
1
+ #! /usr/bin/env ruby
2
+ #
3
+ require_relative '../lib/berklee_valencia'
4
+ #
5
+ BerkleeValencia::CLI.new.call
6
+
7
+
8
+ # ||
9
+ # ||
10
+ # || /||
11
+ # || //||
12
+ # || // ||
13
+ # ||// ||
14
+ # ||/ ||
15
+ # || ||
16
+ # || /||
17
+ # || //||
18
+ # || // ||
19
+ # ||// ||
20
+ # ||/ ||
21
+ # ||
22
+ # ||
@@ -0,0 +1,13 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+ require "open_uri_redirections"
4
+
5
+ require_relative "../lib/berklee_valencia/version"
6
+ require_relative '../lib/berklee_valencia/cli'
7
+ require_relative '../lib/berklee_valencia/scraper'
8
+ require_relative '../lib/berklee_valencia/news_article'
9
+ require_relative '../lib/berklee_valencia/program'
10
+ require_relative '../lib/berklee_valencia/printer'
11
+ require_relative '../lib/berklee_valencia/formatter'
12
+ require_relative '../lib/berklee_valencia/printer'
13
+ require_relative '../lib/berklee_valencia/news_article'
@@ -0,0 +1,4 @@
1
+ module BerkleeValencia
2
+ end
3
+
4
+ require_relative '../config/environment'
@@ -0,0 +1,152 @@
1
+ class BerkleeValencia::CLI
2
+ def call
3
+ puts ""
4
+ puts "||"
5
+ puts "||"
6
+ puts "|| /|| Buenos Dias!"
7
+ puts "|| //||"
8
+ puts "|| // || Berklee Valencia is Berklee College of Music's"
9
+ puts "||// || graduate campus located in Valencia, Spain"
10
+ puts "||/ ||"
11
+ puts "|| || Filled with the"
12
+ puts "|| /|| musicians"
13
+ puts "|| //|| business moguls"
14
+ puts "|| // || and technologists"
15
+ puts "||// || of today and tomorrow..."
16
+ puts "||/ ||"
17
+ puts " || ...there is a wealth of things to discover here!"
18
+ puts " ||"
19
+ menu
20
+ end
21
+
22
+ def menu
23
+ puts ""
24
+ puts " Would you like to see the latest news or are you"
25
+ puts " interested in seeing what programs are on offer?"
26
+ 2.times {puts ""}
27
+ puts " 1. Meet the people and read the latest news"
28
+ puts " 2. See the available Programs"
29
+ puts ""
30
+ list_news_or_programs
31
+ end
32
+
33
+ def list_news_or_programs
34
+ input = gets.strip.downcase
35
+ if input.match(/hasta luego|exit|bye|ciao/)
36
+ goodbye
37
+ else
38
+ puts ""
39
+ puts " Please be patient whilst we get up to date with all our news!"
40
+ puts ""
41
+ puts " ... ... ..."
42
+ puts ""
43
+ if input == "1"
44
+ show_news_categories
45
+ elsif input == "2"
46
+ show_programs
47
+ elsif input == "programs"
48
+ what_next
49
+ else
50
+ say_what
51
+ end
52
+ end
53
+ end
54
+
55
+ def list_news_articles
56
+ puts ""
57
+ input = gets.strip.downcase
58
+ if input.to_i > 0
59
+ puts ""
60
+ puts "----------------------------------------------------------"
61
+ puts "Enter the number of the article you'd like to read in full"
62
+ puts " or type 'menu' to see all options"
63
+ puts "----------------------------------------------------------"
64
+ BerkleeValencia::NEWS_ARTICLE.list_news_articles(input)
65
+ show_article
66
+ elsif input == "programs"
67
+ show_programs
68
+ else
69
+ say_what
70
+ end
71
+ end
72
+
73
+ def show_news_categories
74
+ BerkleeValencia::NEWS_ARTICLE.get_news_categories
75
+ puts ""
76
+ puts "----------------------------------------------"
77
+ puts " Which kind of article are you interested in?"
78
+ puts " Type 'programs' to browse programs instead"
79
+ puts "----------------------------------------------"
80
+ BerkleeValencia::NEWS_ARTICLE.list_news_categories
81
+ list_news_articles
82
+ end
83
+
84
+ def show_programs
85
+ BerkleeValencia::PROGRAM.get_programs
86
+ puts ""
87
+ puts "----------------------------------------------------------------"
88
+ puts " Enter the number of the program you'd like to read more about"
89
+ puts " or type 'news' to browse our latest news and articles."
90
+ puts "----------------------------------------------------------------"
91
+ puts "Graduate Programs"
92
+ BerkleeValencia::PROGRAM.list_programs("grad")
93
+ puts ""
94
+ puts "Other Programs"
95
+ BerkleeValencia::PROGRAM.list_programs("other")
96
+ show_program
97
+ end
98
+
99
+ def show_article
100
+ puts ""
101
+ input = gets.strip.downcase
102
+ if input.to_i > 0
103
+ if BerkleeValencia::NEWS_ARTICLE.print_article(input) == "abort mission!"
104
+ menu
105
+ else
106
+ what_next
107
+ end
108
+ elsif input == "menu"
109
+ what_next
110
+ else
111
+ say_what
112
+ end
113
+ end
114
+
115
+ def show_program
116
+ puts ""
117
+ input = gets.strip.downcase
118
+ if input.to_i > 0
119
+ BerkleeValencia::PROGRAM.print_program(input)
120
+ what_next
121
+ elsif input == "news"
122
+ show_news_categories
123
+ else
124
+ say_what
125
+ end
126
+ end
127
+
128
+ def say_what
129
+ puts ""
130
+ puts "Sorry, I didn't understand that!"
131
+ what_next
132
+ end
133
+
134
+ def what_next
135
+ puts ""
136
+ puts "What would you like to do next?"
137
+ puts "1. See news articles"
138
+ puts "2. See programs"
139
+ puts ""
140
+ puts " If you're all set, you can just say ciao for now and we will "
141
+ puts " see you next time with the latest news from Berklee Valencia!"
142
+ puts ""
143
+ list_news_or_programs
144
+ end
145
+
146
+
147
+ def goodbye
148
+ puts ""
149
+ puts "Hasta luego!"
150
+ puts ""
151
+ end
152
+ end
@@ -0,0 +1,121 @@
1
+ class Formatter
2
+
3
+ def self.wrap(text, width=80)
4
+ text.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
5
+ end
6
+
7
+ class Article < Formatter
8
+ def self.header(article, article_extended)
9
+ border = border_maker(article, article_extended)
10
+ puts ""
11
+ 2.times {puts "#{border}"}
12
+ puts "#{wrap(article[:title])}"
13
+ gap = gap_maker(article, article_extended, border)
14
+ puts "#{article_extended[:author]}#{gap}#{article[:date]}"
15
+ 2.times {puts "#{border}"}
16
+ end
17
+
18
+ def self.border_maker(article, article_extended)
19
+ border = ""
20
+ if article[:title].length < (article_extended[:author].length + article[:date].length)
21
+ borderlength = article_extended[:author].length + article[:date].length + 5
22
+ borderlength.times {border << "-"}
23
+ elsif article[:title].length > 80
24
+ 80.times {border << "-"}
25
+ else
26
+ borderlength = article[:title].length
27
+ borderlength.times {border << "-"}
28
+ end
29
+ border
30
+ end
31
+
32
+ def self.gap_maker(article, article_extended, border)
33
+ gap = ""
34
+ if border.length > article[:title].length
35
+ 5.times {gap << " "}
36
+ else
37
+ (article[:title].length - article_extended[:author].length - article[:date].length).times {gap << " "}
38
+ end
39
+ gap
40
+ end
41
+
42
+ def self.body(article, article_extended)
43
+ article_extended[:body].each do |paragraph|
44
+ if paragraph.match(/-{3} /)
45
+ puts " ______________________________________________________________________________"
46
+ puts " || Press enter to scroll to the next section ||"
47
+ puts " || or type 'menu' to see all options ||"
48
+ puts " ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾"
49
+ input = gets.strip
50
+ if input == "menu"
51
+ return "abort mission!"
52
+ end
53
+ end
54
+
55
+ if !paragraph.downcase.match(/click here/)
56
+ if paragraph.match(/below/)
57
+ puts "#{wrap(paragraph)}"
58
+ puts " Vist #{article_extended[:related_links].shift}"
59
+ elsif paragraph == " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
60
+ puts " Vist #{article_extended[:related_links].shift}"
61
+ puts paragraph
62
+ else
63
+ puts "#{wrap(paragraph)}"
64
+ puts ""
65
+ end #nestedif
66
+ end #if
67
+ end #do
68
+ end #print_body method
69
+
70
+ def self.end
71
+ puts ""
72
+ puts " ______________________________________________________________________________"
73
+ puts " || END OF ARTICLE ||"
74
+ puts " ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾"
75
+ 2.times {puts ""}
76
+ end
77
+ end
78
+
79
+ class Program < Formatter
80
+ def self.header(program)
81
+ border = ""
82
+ (program[:name].gsub("#{program[:detail]}", "").length + 4).times {border << "-"}
83
+ puts ""
84
+ 2.times {puts border}
85
+ puts " #{program[:name].gsub("#{program[:detail]}", "")}"
86
+ if program[:detail] != ""
87
+ puts " (#{program[:detail]})"
88
+ end
89
+ end
90
+
91
+ def self.intro(program_extended)
92
+ puts "--------------------------------------------------------------------------------"
93
+ puts " Introduction"
94
+ puts "--------------------------------------------------------------------------------"
95
+ puts "#{wrap(program_extended[:introduction])}"
96
+ puts ""
97
+ end
98
+
99
+ def self.highlights(program_extended)
100
+ puts "--------------------------------------------------------------------------------"
101
+ puts " Program Highlights"
102
+ puts "--------------------------------------------------------------------------------"
103
+ program_extended[:highlights].each do |highlight|
104
+ puts "| #{highlight[:hl_title]} |"
105
+ puts "#{wrap(highlight[:hl_body])}"
106
+ puts ""
107
+ end
108
+ end
109
+
110
+ def self.more_info(program)
111
+ puts "--------------------------------------------------------------------------------"
112
+ puts " For full program information:"
113
+ puts "--------------------------------------------------------------------------------"
114
+ puts "Visit #{program[:url]}"
115
+ 2.times {puts "--------------------------------------------------------------------------------"}
116
+ 2.times {puts ""}
117
+ end
118
+
119
+ end #Program class
120
+
121
+ end #Formatter
@@ -0,0 +1,39 @@
1
+ class BerkleeValencia::NEWS_ARTICLE
2
+ @@categories
3
+ @@articles
4
+ @@matches
5
+
6
+ def self.get_news_categories
7
+ @@articles = BerkleeValencia::SCRAPER.scrape_news
8
+ @@categories = []
9
+ @@articles.each do |article|
10
+ @@categories.push(article[:category])
11
+ end
12
+ @@categories.uniq!
13
+ end
14
+
15
+ def self.list_news_categories
16
+ @@categories.each.with_index(1) do |category, i|
17
+ puts "#{i}. #{category}"
18
+ end
19
+ end
20
+
21
+ def self.list_news_articles(input)
22
+ @@matches = @@articles.find_all {|article| article[:category] == @@categories[input.to_i-1]}
23
+ @@matches.each.with_index(1) do |article, i|
24
+ puts "#{i}: #{Formatter.wrap(article[:title])}"
25
+ puts " // Posted on #{article[:date]}"
26
+ puts "#{Formatter.wrap(article[:excerpt])}"
27
+ puts ""
28
+ article[:i] = i
29
+ end
30
+ end
31
+
32
+ def self.print_article(input)
33
+ article = @@articles.find { |article| article[:i] == input.to_i }
34
+ url = article[:url]
35
+ article_extended = BerkleeValencia::SCRAPER.scrape_article(url)
36
+ BerkleeValencia::PRINTER.print_article(article, article_extended)
37
+ end
38
+
39
+ end
@@ -0,0 +1,16 @@
1
+ class BerkleeValencia::PRINTER
2
+ def self.print_article(article, article_extended)
3
+ Formatter::Article.header(article, article_extended)
4
+ Formatter::Article.body(article, article_extended)
5
+ Formatter::Article.end
6
+ end
7
+
8
+ def self.print_program(program, program_extended)
9
+ Formatter::Program.header(program)
10
+ Formatter::Program.intro(program_extended)
11
+ if program_extended[:highlights].length > 0
12
+ Formatter::Program.highlights(program_extended)
13
+ end
14
+ Formatter::Program.more_info(program)
15
+ end
16
+ end #class
@@ -0,0 +1,28 @@
1
+ class BerkleeValencia::PROGRAM
2
+ @@grad_programs
3
+ @@other_programs
4
+
5
+ def self.get_programs
6
+ @@grad_programs = BerkleeValencia::SCRAPER.scrape_programs("grad")
7
+ @@other_programs = BerkleeValencia::SCRAPER.scrape_programs("other")
8
+ end
9
+
10
+ def self.list_programs(type)
11
+ type == "grad" ? programs = @@grad_programs : programs = @@other_programs
12
+ programs == @@grad_programs ? i = 1 : i = (@@grad_programs.length + 1)
13
+ programs.each.with_index(i) do |program, i|
14
+ (program[:detail] != "") ? (puts "#{i}: #{program[:name].gsub("#{program[:detail]}", "")} (#{program[:detail]})") : (puts "#{i}: #{program[:name]}")
15
+ program[:i] = i
16
+ end# do
17
+ end #list programs method
18
+
19
+ def self.print_program(input)
20
+ programs = @@grad_programs
21
+ @@other_programs.each { |program| programs << program }
22
+ program = programs.find { |program| program[:i] == input.to_i }
23
+ url = program[:url]
24
+ program_extended = BerkleeValencia::SCRAPER.scrape_program(url)
25
+ BerkleeValencia::PRINTER.print_program(program, program_extended)
26
+ end #method
27
+
28
+ end #class
@@ -0,0 +1,96 @@
1
+ class BerkleeValencia::SCRAPER
2
+ @bv_news = "https://valencia.berklee.edu/news/"
3
+ @bv_programs = "https://valencia.berklee.edu/academic-programs/"
4
+
5
+ def self.scrape_news
6
+ news = Nokogiri::HTML(open(@bv_news))
7
+ articles = []
8
+ news.css("div#news_container div.content").each do |article|
9
+ articles << {
10
+ date: article.css("div.news_excerpt span.date").text,
11
+ title: article.css("div.news_excerpt h3 a").text,
12
+ excerpt: article.css("div.news_excerpt p").text,
13
+ url: article.css("div.news_excerpt h3 a").attribute("href").value,
14
+ category: article.css("span.category_name").text
15
+ }
16
+ end
17
+ articles
18
+ end
19
+
20
+ def self.scrape_programs(type)
21
+ type == "grad" ? academics = Nokogiri::HTML(open(@bv_programs)).css("div.col-3-5").first : academics = Nokogiri::HTML(open(@bv_programs)).css("div.col-3-5").last
22
+ programs = []
23
+ academics.css("ul a").each do |program|
24
+ programs << {
25
+ name: program.text,
26
+ detail: program.css("span").text,
27
+ url: program.attribute("href").value
28
+ }
29
+ end
30
+ programs
31
+ end
32
+
33
+ def self.scrape_article(url)
34
+ article = Nokogiri::HTML(open(url))
35
+ extended_info = {
36
+ author: article.css("span.author").text,
37
+ related_links: [],
38
+ body: []
39
+ }
40
+ sort_content(article, extended_info)
41
+ end
42
+
43
+ def self.sort_content(article, extended_info)
44
+ article.css("div#tab_intro p").each do |para|
45
+ if para.css("iframe").length == 0 && para.css("em").text.length < 30 && para.css("strong").text == "" && !para.text.match(/•/)
46
+ extended_info[:body] << para.text
47
+ elsif para.css("strong").text != ""
48
+ extended_info[:body] << " --- #{para.text} ---"
49
+ elsif para.css("em").text != ""
50
+ comment = "#{para.text}"
51
+ extended_info[:body] << " - - - - - - - - - - - - - - - - - Media - - - - - - - - - - - - - - - - - - -"
52
+ extended_info[:body] << comment
53
+ extended_info[:body] << " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
54
+ elsif para.css("iframe").length > 0
55
+ extended_info[:related_links] << para.css("iframe").attribute("src").value
56
+ end
57
+ end
58
+ extended_info
59
+ end
60
+
61
+ def self.scrape_program(url)
62
+ program = Nokogiri::HTML(open(url, :allow_redirections => :all))
63
+ extended_info = {
64
+ highlights: [],
65
+ list: []
66
+ }
67
+ scrape_program_intro(program, extended_info)
68
+ scrape_program_highlights(program, extended_info)
69
+ end
70
+
71
+ def self.scrape_program_intro(program, extended_info)
72
+ # scrape intro
73
+ if program.css("div#tab_intro p").first.text.length > 0
74
+ extended_info[:introduction] = program.css("div#tab_intro p").first.text
75
+ elsif program.css("div#tab_intro h4").length > 0
76
+ extended_info[:introduction] = program.css("div#tab_intro h4").first.text
77
+ else
78
+ intro = program.css("div#tab_intro p").detect {|p| p.text.length > 150}
79
+ extended_info[:introduction] = intro.text
80
+ end
81
+ end
82
+
83
+ def self.scrape_program_highlights(program, extended_info)
84
+ program.css("div#tab_intro div.block_content").each do |highlight|
85
+ hl_title = highlight.css("p.block_content_item_title").text
86
+ hl_body = highlight.css("div.bk_txt").text
87
+ extended_info[:highlights] << {
88
+ hl_title: hl_title,
89
+ hl_body: hl_body
90
+ }
91
+ end
92
+ extended_info
93
+ end
94
+
95
+
96
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: berklee-valencia
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gingertonic
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
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: nokogiri
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '1.8'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '1.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: open_uri_redirections
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: |-
98
+ Hello! Berklee Valencia is Berklee College of Music's graduate campus located in Valencia, Spain.
99
+ Filled with the musicians, business moguls and technologists of today and tomorrow, there is a wealth of things to discover here.
100
+ email: bethmschofield@gmail.com
101
+ executables:
102
+ - berklee-valencia
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - bin/berklee-valencia
107
+ - config/environment.rb
108
+ - lib/berklee_valencia.rb
109
+ - lib/berklee_valencia/cli.rb
110
+ - lib/berklee_valencia/formatter.rb
111
+ - lib/berklee_valencia/news_article.rb
112
+ - lib/berklee_valencia/printer.rb
113
+ - lib/berklee_valencia/program.rb
114
+ - lib/berklee_valencia/scraper.rb
115
+ homepage: http://rubygems.org/gems/berklee-valencia
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.7.6
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: CLI gem for user to read latest Berklee Valencia news and about programs
139
+ offered.
140
+ test_files: []