urbanopt-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/change_log.rb ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'github_api'
4
+ require 'date'
5
+ require 'optparse'
6
+ require 'optparse/date'
7
+
8
+ # Instructions:
9
+ # Get a token from github's settings (https://github.com/settings/tokens)
10
+ #
11
+ # Example:
12
+ # ruby change_log.rb -t abcdefghijklmnopqrstuvwxyz -s 2017-09-06
13
+ #
14
+
15
+ options = {}
16
+ OptionParser.new do |opts|
17
+ opts.banner = "Usage: change_log.rb [options]\n" +
18
+ "Prints New, Open, Closed Issues, and number of accepted PRs"
19
+ opts.separator ""
20
+
21
+ # defaults, go back 90 days
22
+ options[:start_date] = Date.today - 90
23
+ options[:end_date] = Date.today
24
+
25
+ opts.on('-s', '--start-date [DATE]', Date, 'Start of data (e.g. 2017-09-06)') do |v|
26
+ options[:start_date] = v
27
+ end
28
+ opts.on('-e', '--end-date [DATE]', Date, 'End of data (e.g. 2017-09-13)') do |v|
29
+ options[:end_date] = v
30
+ end
31
+ opts.on('-t', '--token [String]', String, 'Github API Token') do |v|
32
+ options[:token] = v
33
+ end
34
+ end.parse!
35
+
36
+ # Convert dates to time objects
37
+ options[:start_date] = Time.parse(options[:start_date].to_s)
38
+ options[:end_date] = Time.parse(options[:end_date].to_s)
39
+ puts options
40
+
41
+ ### Repository options
42
+ repo_owner = 'URBANopt'
43
+ repo = 'uo-cli'
44
+
45
+ github = Github.new
46
+ if options[:token]
47
+ puts 'Using github token'
48
+ github = Github.new oauth_token: options[:token]
49
+ end
50
+
51
+ total_open_issues = []
52
+ total_open_pull_requests = []
53
+ new_issues = []
54
+ closed_issues = []
55
+ accepted_pull_requests = []
56
+
57
+ def get_num(issue)
58
+ issue.html_url.split('/')[-1].to_i
59
+ end
60
+
61
+ def get_issue_num(issue)
62
+ "\##{get_num(issue)}"
63
+ end
64
+
65
+ def get_html_url(issue)
66
+ issue.html_url
67
+ end
68
+
69
+ def get_title(issue)
70
+ issue.title
71
+ end
72
+
73
+ def print_issue(issue)
74
+ is_feature = false
75
+ issue.labels.each { |label| is_feature = true if label.name == 'Feature Request' }
76
+
77
+ if is_feature
78
+ "- Improved [#{get_issue_num(issue)}]( #{get_html_url(issue)} ), #{get_title(issue)}"
79
+ else
80
+ "- Fixed [#{get_issue_num(issue)}]( #{get_html_url(issue)} ), #{get_title(issue)}"
81
+ end
82
+ end
83
+
84
+ # Process Open Issues
85
+ results = -1
86
+ page = 1
87
+ while results != 0
88
+ resp = github.issues.list user: repo_owner, repo: repo, sort: 'created', direction: 'asc',
89
+ state: 'open', per_page: 100, page: page
90
+ results = resp.length
91
+ resp.env[:body].each do |issue, _index|
92
+ created = Time.parse(issue.created_at)
93
+ if !issue.key?(:pull_request)
94
+ total_open_issues << issue
95
+ if created >= options[:start_date] && created <= options[:end_date]
96
+ new_issues << issue
97
+ end
98
+ else
99
+ total_open_pull_requests << issue
100
+ end
101
+ end
102
+
103
+ page += 1
104
+ end
105
+
106
+ # Process Closed Issues
107
+ results = -1
108
+ page = 1
109
+ while results != 0
110
+ resp = github.issues.list user: repo_owner, repo: repo, sort: 'created', direction: 'asc',
111
+ state: 'closed', per_page: 100, page: page
112
+ results = resp.length
113
+ resp.env[:body].each do |issue, _index|
114
+ created = Time.parse(issue.created_at)
115
+ closed = Time.parse(issue.closed_at)
116
+ if !issue.key?(:pull_request)
117
+ if created >= options[:start_date] && created <= options[:end_date]
118
+ new_issues << issue
119
+ end
120
+ if closed >= options[:start_date] && closed <= options[:end_date]
121
+ closed_issues << issue
122
+ end
123
+ elsif closed >= options[:start_date] && closed <= options[:end_date]
124
+ accepted_pull_requests << issue
125
+ end
126
+ end
127
+
128
+ page += 1
129
+ end
130
+
131
+ closed_issues.sort! { |x, y| get_num(x) <=> get_num(y) }
132
+ new_issues.sort! { |x, y| get_num(x) <=> get_num(y) }
133
+ accepted_pull_requests.sort! { |x, y| get_num(x) <=> get_num(y) }
134
+ total_open_pull_requests.sort! { |x, y| get_num(x) <=> get_num(y) }
135
+
136
+ puts "Total Open Issues: #{total_open_issues.length}"
137
+ puts "Total Open Pull Requests: #{total_open_pull_requests.length}"
138
+ puts "\nDate Range: #{options[:start_date].strftime('%m/%d/%y')} - #{options[:end_date].strftime('%m/%d/%y')}:"
139
+ puts "\nNew Issues: #{new_issues.length} (" + new_issues.map { |issue| get_issue_num(issue) }.join(', ') + ')'
140
+
141
+ puts "\nClosed Issues: #{closed_issues.length}"
142
+ closed_issues.each { |issue| puts print_issue(issue) }
143
+
144
+ puts "\nAccepted Pull Requests: #{accepted_pull_requests.length}"
145
+ accepted_pull_requests.each { |issue| puts print_issue(issue) }
146
+
147
+ puts "\nAll Open Issues: #{total_open_issues.length} (" + total_open_issues.map { |issue| get_issue_num(issue) }.join(', ') + ')'
@@ -0,0 +1,5 @@
1
+ module URBANopt
2
+ module CLI
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
data/lib/uo_cli.rb ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/ ruby
2
+
3
+ #*********************************************************************************
4
+ # URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
5
+ # contributors. All rights reserved.
6
+ #
7
+ # Redistribution and use in source and binary forms, with or without modification,
8
+ # are permitted provided that the following conditions are met:
9
+ #
10
+ # Redistributions of source code must retain the above copyright notice, this list
11
+ # of conditions and the following disclaimer.
12
+ #
13
+ # Redistributions in binary form must reproduce the above copyright notice, this
14
+ # list of conditions and the following disclaimer in the documentation and/or other
15
+ # materials provided with the distribution.
16
+ #
17
+ # Neither the name of the copyright holder nor the names of its contributors may be
18
+ # used to endorse or promote products derived from this software without specific
19
+ # prior written permission.
20
+ #
21
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24
+ # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
25
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
30
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #*********************************************************************************
32
+
33
+ require "uo_cli/version"
34
+ require "optparse"
35
+ require "urbanopt/geojson"
36
+ require "urbanopt/scenario"
37
+ require "csv"
38
+ require "json"
39
+ require "openssl"
40
+
41
+
42
+ module URBANopt
43
+ module CLI
44
+
45
+ # Set up user interface
46
+ @user_input = {}
47
+ the_parser = OptionParser.new do |opts|
48
+ opts.banner = "Usage: uo [-pmradsfv]\n" +
49
+ "\n" +
50
+ "URBANopt CLI. \n" +
51
+ "First create a project folder with -p, then run additional commands as desired \n"
52
+ opts.separator ""
53
+
54
+ opts.on("-p", "--project_folder <DIR>",String, "Create project directory named <DIR> in your current folder") do |folder|
55
+ @user_input[:project_folder] = folder
56
+ end
57
+ opts.on("-m", "--make_scenario", String, "Create ScenarioCSV files for each MapperFile using the Feature file path. Must specify -f argument\n" +
58
+ " Example: uo -m -f example_project.json\n" +
59
+ " You must be insde the project directory you just created for this to work") do
60
+ @user_input[:make_scenario_from] = "Create scenario files from FeatureFiles according to the MapperFiles in the 'mappers' directory" # This text does not get displayed to the user
61
+ end
62
+ opts.on("-r", "--run", String, "Run simulations. Must specify -s & -f arguments\n" +
63
+ " Example: uo -r -s baseline_scenario.csv -f example_project.json") do
64
+ @user_input[:run_scenario] = "Run simulations" # This text does not get displayed to the user
65
+ end
66
+ opts.on("-a", "--aggregate", String, "Aggregate individual feature results to scenario-level results. Must specify -s & -f arguments\n" +
67
+ " Example: uo -a -s baseline_scenario.csv -f example_project.json") do
68
+ @user_input[:aggregate] = "Aggregate all features to a whole Scenario" # This text does not get displayed to the user
69
+ end
70
+ opts.on("-d", "--delete_scenario", String, "Delete results from scenario. Must specify -s argument\n" +
71
+ " Example: uo -d -s baseline_scenario.csv") do
72
+ @user_input[:delete_scenario] = "Delete scenario results that were created from <SFP>" # This text does not get displayed to the user
73
+ end
74
+ opts.on("-s", "--scenario_file <SFP>", String, "Specify <SFP> (ScenarioCSV file path). Used as input for other commands") do |scenario|
75
+ @user_input[:scenario] = scenario
76
+ end
77
+ opts.on("-f", "--feature_file <FFP>", String, "Specify <FFP> (Feature file path). Used as input for other commands") do |feature|
78
+ @user_input[:feature] = feature
79
+ end
80
+ opts.on("-v", "--version", "Show CLI version and exit") do
81
+ @user_input[:version_request] = VERSION
82
+ end
83
+ end
84
+
85
+ begin
86
+ the_parser.parse!
87
+ rescue OptionParser::InvalidOption => e
88
+ puts e
89
+ end
90
+
91
+
92
+ # Simulate energy usage for each Feature in the Scenario\
93
+ # params\
94
+ # +scenario+:: _string_ Path to csv file that defines the scenario\
95
+ # +feature_file_path+:: _string_ Path to Feature File used to describe set of features in the district
96
+ #
97
+ # FIXME: This only works when scenario_file and feature_file are in the project root directory
98
+ # Also, feels a little weird that now I'm only using instance variables and not passing anything to this function. I guess it's ok?
99
+ def self.run_func
100
+ name = "#{@scenario_name.split('.')[0].capitalize}"
101
+ root_dir = File.absolute_path(@scenario_root)
102
+ run_dir = File.join(root_dir, 'run', name.downcase)
103
+ csv_file = File.join(root_dir, @scenario_name)
104
+ featurefile = File.join(root_dir, @feature_name)
105
+ mapper_files_dir = File.join(root_dir, "mappers")
106
+ num_header_rows = 1
107
+
108
+ feature_file = URBANopt::GeoJSON::GeoFile.from_file(featurefile)
109
+ scenario_output = URBANopt::Scenario::ScenarioCSV.new(name, root_dir, run_dir, feature_file, mapper_files_dir, csv_file, num_header_rows)
110
+ return scenario_output
111
+ end
112
+
113
+ # Create a scenario csv file from a FeatureFile
114
+ # params\
115
+ # +feature_file_path+:: _string_ Path to a FeatureFile
116
+ def self.create_scenario_csv_file(feature_file_path)
117
+ feature_file_json = JSON.parse(File.read(feature_file_path), :symbolize_names => true)
118
+ Dir["#{@feature_root}/mappers/*.rb"].each do |mapper_file|
119
+ mapper_root, mapper_base = File.split(mapper_file)
120
+ mapper_name = mapper_base.split('.')[0]
121
+ scenario_file_name = "#{mapper_name.downcase}_scenario.csv"
122
+ CSV.open(File.join(@feature_root, scenario_file_name), "wb", :write_headers => true,
123
+ :headers => ["Feature Id","Feature Name","Mapper Class"]) do |csv|
124
+ feature_file_json[:features].each do |feature|
125
+ csv << [feature[:properties][:id], feature[:properties][:name], "URBANopt::Scenario::#{mapper_name}Mapper"]
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+
132
+ # Create project folder
133
+ # params\
134
+ # +dir_name+:: _string_ Name of new project folder
135
+ #
136
+ # Folder gets created in the current working directory
137
+ # Includes weather for UO's example location, a base workflow file, and mapper files to show a baseline and a high-efficiency option.
138
+ def self.create_project_folder(dir_name)
139
+ if Dir.exist?(dir_name)
140
+ abort("ERROR: there is already a directory here named #{dir_name}... aborting")
141
+ else
142
+ puts "CREATING URBANopt project directory: #{dir_name}"
143
+ Dir.mkdir dir_name
144
+ Dir.mkdir File.join(dir_name, 'mappers')
145
+ Dir.mkdir File.join(dir_name, 'weather')
146
+ mappers_dir_abs_path = File.absolute_path(File.join(dir_name, 'mappers/'))
147
+ weather_dir_abs_path = File.absolute_path(File.join(dir_name, 'weather/'))
148
+
149
+ # FIXME: When residential hpxml flow is implemented (https://github.com/urbanopt/urbanopt-example-geojson-project/pull/24 gets merged) these files will change
150
+ example_feature_file = "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/example_project.json"
151
+ example_gem_file = "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/Gemfile"
152
+ remote_mapper_files = [
153
+ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/mappers/base_workflow.osw",
154
+ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/mappers/Baseline.rb",
155
+ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/mappers/HighEfficiency.rb",
156
+ ]
157
+ remote_weather_files = [
158
+ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/weather/USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.epw",
159
+ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/weather/USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.ddy",
160
+ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/weather/USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.stat",
161
+ ]
162
+
163
+ # Download files to user's local machine
164
+ remote_mapper_files.each do |mapper_file|
165
+ mapper_root, mapper_base = File.split(mapper_file)
166
+ mapper_download = open(mapper_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE})
167
+ IO.copy_stream(mapper_download, File.join(mappers_dir_abs_path, mapper_base))
168
+ end
169
+ remote_weather_files.each do |weather_file|
170
+ weather_root, weather_base = File.split(weather_file)
171
+ weather_download = open(weather_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE})
172
+ IO.copy_stream(weather_download, File.join(weather_dir_abs_path, weather_base))
173
+ end
174
+ gem_root, gem_base = File.split(example_gem_file)
175
+ example_gem_download = open(example_gem_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE})
176
+ IO.copy_stream(example_gem_download, File.join(dir_name, gem_base))
177
+
178
+ feature_root, feature_base = File.split(example_feature_file)
179
+ example_feature_download = open(example_feature_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE})
180
+ IO.copy_stream(example_feature_download, File.join(dir_name, feature_base))
181
+ end
182
+ end
183
+
184
+
185
+ # Perform CLI actions
186
+ if @user_input[:project_folder]
187
+ create_project_folder(@user_input[:project_folder])
188
+ puts "\nAn example FeatureFile is included: 'example_project.json'. You may place your own FeatureFile alongside the example."
189
+ puts "Weather data is provided for the example FeatureFile. Additional weather data files may be downloaded from energyplus.net/weather for free"
190
+ puts "If you use additional weather files, ensure they are added to the 'weather' directory. You will need to configure your mapper file and your osw file to use the desired weather file"
191
+ puts "Next, move inside your new folder ('cd <FolderYouJustCreated>') and create ScenarioFiles using this CLI: 'uo -m -f <FFP>'"
192
+ end
193
+
194
+ if @user_input[:make_scenario_from]
195
+ if @user_input[:feature].nil?
196
+ abort("\nYou must provide the '-s' flag and a valid path to a FeatureFile!\n---\n\n")
197
+ end
198
+ @feature_root, @feature_name = File.split(@user_input[:feature])
199
+ puts "\nBuilding sample ScenarioFiles, assigning mapper classes to each feature from #{@feature_name}..."
200
+ create_scenario_csv_file(@user_input[:feature])
201
+ puts "Done"
202
+ end
203
+
204
+ if @user_input[:run_scenario]
205
+ if @user_input[:scenario].nil?
206
+ abort("\nYou must provide '-s' flag and a valid path to a ScenarioFile!\n---\n\n")
207
+ end
208
+ if @user_input[:feature].nil?
209
+ abort("\nYou must provide '-f' flag and a valid path to a FeatureFile!\n---\n\n")
210
+ end
211
+ @scenario_root, @scenario_name = File.split(@user_input[:scenario])
212
+ @feature_root, @feature_name = File.split(@user_input[:feature])
213
+ puts "\nSimulating features of '#{@feature_name}' as directed by '#{@scenario_name}'...\n\n"
214
+ scenario_runner = URBANopt::Scenario::ScenarioRunnerOSW.new
215
+ scenario_runner.run(run_func())
216
+ puts "Done"
217
+ end
218
+
219
+ if @user_input[:aggregate]
220
+ if @user_input[:scenario].nil?
221
+ abort("\nYou must provide '-s' flag and a valid path to a ScenarioFile!\n---\n\n")
222
+ end
223
+ if @user_input[:feature].nil?
224
+ abort("\nYou must provide '-f' flag and a valid path to a FeatureFile!\n---\n\n")
225
+ end
226
+ @scenario_root, @scenario_name = File.split(@user_input[:scenario])
227
+ @feature_root, @feature_name = File.split(@user_input[:feature])
228
+ puts "\nAggregating results across all features of #{@feature_name} according to '#{@scenario_name}'..."
229
+ scenario_result = URBANopt::Scenario::ScenarioDefaultPostProcessor.new(run_func()).run
230
+ scenario_result.save
231
+ puts "Done"
232
+ end
233
+
234
+ if @user_input[:delete_scenario]
235
+ if @user_input[:scenario].nil?
236
+ abort("\nYou must provide '-s' flag and a valid path to a ScenarioFile!\n---\n\n")
237
+ end
238
+ @scenario_root, @scenario_name = File.split(@user_input[:scenario])
239
+ scenario_name = @scenario_name.split('.')[0]
240
+ scenario_root = File.absolute_path(@scenario_root)
241
+ scenario_results_dir = File.join(scenario_root, 'run', scenario_name)
242
+ puts "\nDeleting previous results from '#{@scenario_name}'..."
243
+ FileUtils.rm_rf(scenario_results_dir)
244
+ puts "Done"
245
+ end
246
+
247
+ if @user_input[:version_request]
248
+ puts "URBANopt CLI version: #{@user_input[:version_request]}"
249
+ end
250
+
251
+ end # End module CLI
252
+
253
+ end # End module Urbanopt
data/uo_cli.gemspec ADDED
@@ -0,0 +1,44 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "uo_cli/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "urbanopt-cli"
8
+ spec.version = URBANopt::CLI::VERSION
9
+ spec.authors = ["Nathan Moore"]
10
+ spec.email = ["nathan.moore@nrel.gov"]
11
+
12
+ spec.summary = "URBANopt CLI"
13
+ spec.description = "Interfacing with URBANopt"
14
+ spec.homepage = "https://docs.urbanopt.net/"
15
+
16
+ if spec.respond_to?(:metadata)
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org/"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ else
20
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
21
+ end
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = "bin"
29
+ # spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.executables = ["uo"]
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "urbanopt-scenario", "~> 0.1.1"
34
+ spec.add_dependency "urbanopt-geojson", "~> 0.1.0"
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.17"
37
+ spec.add_development_dependency "rake", "~> 12.3"
38
+ spec.add_development_dependency "rspec", "~> 3.0"
39
+ spec.add_development_dependency "github_api", "~> 0.18.0"
40
+
41
+ # Set specific version while we are on Ruby 2.2.4
42
+ spec.add_development_dependency "rack", "2.1.2"
43
+
44
+ end