mortar 0.9.3 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/lib/mortar/auth.rb +0 -8
  2. data/lib/mortar/command/local.rb +14 -2
  3. data/lib/mortar/generators/characterize_generator.rb +105 -0
  4. data/lib/mortar/generators/generator_base.rb +1 -1
  5. data/lib/mortar/generators/project_generator.rb +0 -4
  6. data/lib/mortar/helpers.rb +13 -0
  7. data/lib/mortar/local/controller.rb +11 -3
  8. data/lib/mortar/local/installutil.rb +66 -6
  9. data/lib/mortar/local/jython.rb +11 -5
  10. data/lib/mortar/local/pig.rb +6 -9
  11. data/lib/mortar/local/python.rb +43 -23
  12. data/lib/mortar/plugin.rb +1 -0
  13. data/lib/mortar/templates/characterize/README.md +7 -0
  14. data/lib/mortar/templates/characterize/controlscripts/lib/__init__.py +0 -0
  15. data/lib/mortar/templates/characterize/controlscripts/lib/characterize_control.py +27 -0
  16. data/lib/mortar/templates/characterize/fixtures/gitkeep +0 -0
  17. data/lib/mortar/templates/characterize/gitignore +8 -0
  18. data/lib/mortar/templates/{project → characterize}/macros/characterize_macro.pig +0 -0
  19. data/lib/mortar/templates/characterize/macros/gitkeep +0 -0
  20. data/lib/mortar/templates/{project → characterize}/pigscripts/characterize.pig +0 -0
  21. data/lib/mortar/templates/characterize/pigscripts/pigscript.pig +38 -0
  22. data/lib/mortar/templates/characterize/udfs/java/gitkeep +0 -0
  23. data/lib/mortar/templates/characterize/udfs/jython/gitkeep +0 -0
  24. data/lib/mortar/templates/{project → characterize}/udfs/jython/top_5_tuple.py +0 -0
  25. data/lib/mortar/templates/characterize/udfs/python/python_udf.py +13 -0
  26. data/lib/mortar/templates/characterize/vendor/controlscripts/lib/__init__.py +0 -0
  27. data/lib/mortar/templates/characterize/vendor/macros/gitkeep +0 -0
  28. data/lib/mortar/templates/characterize/vendor/pigscripts/gitkeep +0 -0
  29. data/lib/mortar/templates/characterize/vendor/udfs/java/gitkeep +0 -0
  30. data/lib/mortar/templates/characterize/vendor/udfs/jython/gitkeep +0 -0
  31. data/lib/mortar/templates/characterize/vendor/udfs/python/gitkeep +0 -0
  32. data/lib/mortar/version.rb +1 -1
  33. data/spec/mortar/command/local_spec.rb +29 -2
  34. data/spec/mortar/command/projects_spec.rb +0 -4
  35. data/spec/mortar/local/installutil_spec.rb +69 -1
  36. data/spec/mortar/local/pig_spec.rb +2 -2
  37. metadata +173 -132
  38. data/lib/mortar/templates/project/controlscripts/lib/characterize_control.py +0 -23
data/lib/mortar/auth.rb CHANGED
@@ -67,14 +67,6 @@ class Mortar::Auth
67
67
  end
68
68
  end
69
69
  end
70
-
71
- def default_host
72
- "mortardata.com"
73
- end
74
-
75
- def host
76
- ENV['MORTAR_HOST'] || default_host
77
- end
78
70
 
79
71
  def reauthorize
80
72
  @credentials = ask_for_and_save_credentials
@@ -16,6 +16,7 @@
16
16
 
17
17
  require "mortar/local/controller"
18
18
  require "mortar/command/base"
19
+ require "mortar/generators/characterize_generator"
19
20
 
20
21
  # run select pig commands on your local machine
21
22
  #
@@ -71,7 +72,7 @@ class Mortar::Command::Local < Mortar::Command::Base
71
72
  ctrl.run(script, pig_parameters)
72
73
  end
73
74
 
74
- # local:characterize
75
+ # local:characterize -f PARAMFILE
75
76
  #
76
77
  # Characterize will inspect your input data, inferring a schema and
77
78
  # generating keys, if needed. It will output CSV containing various
@@ -80,7 +81,7 @@ class Mortar::Command::Local < Mortar::Command::Base
80
81
  # -f, --param-file PARAMFILE # Load pig parameter values from a file
81
82
  #
82
83
  # Load some data and emit statistics.
83
- # PARAMFILE:
84
+ # PARAMFILE (Required):
84
85
  # LOADER=<full class path of loader function>
85
86
  # INPUT_SRC=<Location of the input data>
86
87
  # OUTPUT_PATH=<Relative path from project root for output>
@@ -95,17 +96,28 @@ class Mortar::Command::Local < Mortar::Command::Base
95
96
  def characterize
96
97
  validate_arguments!
97
98
 
99
+ unless options[:param_file]
100
+ error("Usage: mortar local:characterize -f PARAMFILE.\nMust specify parameter file. For detailed help run:\n\n mortar local:characterize -h")
101
+ end
102
+
98
103
  #cd into the project root
99
104
  project_root = options[:project_root] ||= Dir.getwd
100
105
  unless File.directory?(project_root)
101
106
  error("No such directory #{project_root}")
102
107
  end
108
+
103
109
  Dir.chdir(project_root)
104
110
 
111
+ gen = Mortar::Generators::CharacterizeGenerator.new
112
+ gen.generate_characterize
113
+
105
114
  controlscript_name = "controlscripts/lib/characterize_control.py"
115
+ gen = Mortar::Generators::CharacterizeGenerator.new
116
+ gen.generate_characterize
106
117
  script = validate_script!(controlscript_name)
107
118
  ctrl = Mortar::Local::Controller.new
108
119
  ctrl.run(script, pig_parameters)
120
+ gen.cleanup_characterize(project_root)
109
121
  end
110
122
 
111
123
  # local:illustrate PIGSCRIPT [ALIAS]
@@ -0,0 +1,105 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require "fileutils"
18
+ require "mortar/generators/generator_base"
19
+ module Mortar
20
+ module Generators
21
+ class CharacterizeGenerator < Base
22
+
23
+ def generate_characterize
24
+ begin
25
+ inside "pigscripts" do
26
+ copy_file_char "characterize.pig", "characterize.pig"
27
+ end
28
+
29
+ inside "controlscripts" do
30
+ inside "lib" do
31
+ copy_file_char "characterize_control.py", "characterize_control.py"
32
+ end
33
+ end
34
+
35
+ inside "macros" do
36
+ copy_file_char "characterize_macro.pig", "characterize_macro.pig"
37
+ end
38
+
39
+ inside "udfs" do
40
+ inside "jython" do
41
+ copy_file_char "top_5_tuple.py", "top_5_tuple.py"
42
+ end
43
+ end
44
+
45
+ # how best to handle exceptions, here?
46
+ rescue => e
47
+ display("\nCharacterize script generation failed.\n\n")
48
+ raise e
49
+ end
50
+ end
51
+
52
+ def cleanup_characterize(project_root)
53
+ @src_path = project_root
54
+ begin
55
+ inside "pigscripts" do
56
+ remove_file "characterize.pig"
57
+ end
58
+
59
+ inside "controlscripts" do
60
+ inside "lib" do
61
+ remove_file "characterize_control.py"
62
+ end
63
+ end
64
+
65
+ inside "macros" do
66
+ remove_file "characterize_macro.pig"
67
+ end
68
+
69
+ inside "udfs" do
70
+ inside "jython" do
71
+ remove_file "top_5_tuple.py"
72
+ end
73
+ end
74
+
75
+ # how best to handle exceptions, here?
76
+ rescue => e
77
+ display("\nCharacterize script cleanup failed.\n\n")
78
+ raise e
79
+ end
80
+ end
81
+
82
+ def remove_file(target_file, options={ :recursive => false })
83
+ target_path = File.join(@src_path, @rel_path, target_file)
84
+ msg = File.join(@rel_path, target_file)[1..-1]
85
+
86
+ if File.exists?(target_path)
87
+ FileUtils.rm(target_path)
88
+ end
89
+ end
90
+
91
+ def copy_file_char(src_file, dest_file, options={ :recursive => false })
92
+ src_path = File.join(@src_path, @rel_path, src_file)
93
+ dest_path = File.join(@dest_path, @rel_path, dest_file)
94
+ msg = File.join(@rel_path, dest_file)[1..-1]
95
+
96
+ unless File.exists?(dest_path) and
97
+ FileUtils.compare_file(src_path, dest_path)
98
+ FileUtils.mkdir_p(File.dirname(dest_path)) if options[:recursive]
99
+ FileUtils.cp(src_path, dest_path)
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -104,4 +104,4 @@ module Mortar
104
104
 
105
105
  end
106
106
  end
107
- end
107
+ end
@@ -38,7 +38,6 @@ module Mortar
38
38
 
39
39
  inside "pigscripts" do
40
40
  generate_file "pigscript.pig", "#{project_name}.pig"
41
- copy_file "characterize.pig", "characterize.pig"
42
41
  end
43
42
 
44
43
  mkdir "controlscripts"
@@ -47,7 +46,6 @@ module Mortar
47
46
  mkdir "lib"
48
47
  inside "lib" do
49
48
  copy_file "__init__.py", "__init__.py"
50
- copy_file "characterize_control.py", "characterize_control.py"
51
49
  end
52
50
  end
53
51
 
@@ -55,7 +53,6 @@ module Mortar
55
53
 
56
54
  inside "macros" do
57
55
  copy_file "gitkeep", ".gitkeep"
58
- copy_file "characterize_macro.pig", "characterize_macro.pig"
59
56
  end
60
57
 
61
58
  mkdir "fixtures"
@@ -75,7 +72,6 @@ module Mortar
75
72
  mkdir "jython"
76
73
  inside "jython" do
77
74
  copy_file "gitkeep", ".gitkeep"
78
- copy_file "top_5_tuple.py", "top_5_tuple.py"
79
75
  end
80
76
 
81
77
  mkdir "java"
@@ -38,6 +38,18 @@ module Mortar
38
38
  RUBY_PLATFORM =~ /-darwin\d/
39
39
  end
40
40
 
41
+ def default_host
42
+ "mortardata.com"
43
+ end
44
+
45
+ def host
46
+ ENV['MORTAR_HOST'] || default_host
47
+ end
48
+
49
+ def test_name
50
+ ENV['MORTAR_TEST_NAME']
51
+ end
52
+
41
53
  def write_to_file(str_data, path, mkdir_p=true)
42
54
  if mkdir_p
43
55
  FileUtils.mkdir_p File.dirname(path)
@@ -528,5 +540,6 @@ module Mortar
528
540
  create_display_method("exists", "1;34")
529
541
  create_display_method("identical", "1;34")
530
542
  create_display_method("conflict", "1;31")
543
+ create_display_method("remove", "1;35")
531
544
  end
532
545
  end
@@ -33,9 +33,13 @@ Linux systems please consult the documentation on your relevant package manager.
33
33
  EOF
34
34
 
35
35
  NO_PYTHON_ERROR_MESSAGE = <<EOF
36
- A suitable python installation with virtualenv could not be located. Please ensure
37
- you have python 2.6+ installed on your local system. If you need to obtain a copy
38
- of virtualenv it can be located here:
36
+ A suitable python installation could not be located. Please ensure you have python 2.6+
37
+ installed on your local system.
38
+ EOF
39
+
40
+ NO_VIRTENV_ERROR_MESSAGE = <<EOF
41
+ A suitable Python installation was found, but it is required that virtualenv be installed
42
+ as well. You can install it with pip, or download it directly from:
39
43
  https://pypi.python.org/pypi/virtualenv
40
44
  EOF
41
45
 
@@ -86,6 +90,10 @@ EOF
86
90
  error(NO_PYTHON_ERROR_MESSAGE)
87
91
  end
88
92
 
93
+ unless py.check_virtualenv
94
+ error(NO_VIRTENV_ERROR_MESSAGE)
95
+ end
96
+
89
97
  unless py.setup_project_python_environment
90
98
  msg = "\nUnable to setup a python environment with your dependencies, "
91
99
  msg += "see #{py.pip_error_log_path} for more details"
@@ -107,13 +107,73 @@ module Mortar
107
107
  end
108
108
  end
109
109
 
110
- # Downloads the file at a specified url into the supplied director
111
- def download_file(url, dest_dir)
112
- dest_file_path = dest_dir + "/" + File.basename(url)
110
+ # Downloads the file at a specified url into the supplied directory
111
+ def download_file(url, dest_file_path)
112
+ response = get_resource(url)
113
+
113
114
  File.open(dest_file_path, "wb") do |dest_file|
114
- contents = Excon.get(url).body
115
- dest_file.write(contents)
115
+ dest_file.write(response.body)
116
116
  end
117
+
118
+ end
119
+
120
+ # Perform a get request to a url and follow redirects if necessary.
121
+ def get_resource(url)
122
+ make_call(url, 'get')
123
+ end
124
+
125
+ # Perform a head request to a url and follow redirects if necessary.
126
+ def head_resource(url)
127
+ make_call(url, 'head')
128
+ end
129
+
130
+ # Make a request to a mortar resource url. Check response for a
131
+ # redirect and if necessary call the new url. Excon doesn't currently
132
+ # support automatically following redirects. Adds parameter that
133
+ # checks an environment variable to identify the test making this call
134
+ # (if being run by a test).
135
+ def make_call(url, call_func, redirect_times=0, errors=0)
136
+ if redirect_times >= 5
137
+ raise RuntimeError, "Too many redirects. Last url: #{url}"
138
+ end
139
+
140
+ if errors >= 5
141
+ raise RuntimeError, "Server Error at #{url}"
142
+ end
143
+
144
+
145
+ query = {}
146
+ if test_name
147
+ query[:test_name] = test_name
148
+ end
149
+
150
+ headers = {'User-Agent' => Mortar::USER_AGENT}
151
+ if call_func == 'head'
152
+ response = Excon.head( url,
153
+ :headers => headers,
154
+ :query => query
155
+ )
156
+ elsif call_func == 'get'
157
+ response = Excon.get( url,
158
+ :headers => headers,
159
+ :query => query
160
+ )
161
+ else
162
+ raise RuntimeError, "Unknown call type: #{call_func}"
163
+ end
164
+ case response.status
165
+ when 300..303 then
166
+ make_call(response.headers['Location'], call_func, redirect_times+1, errors)
167
+ when 500..599 then
168
+ sleep(make_call_sleep_seconds)
169
+ make_call(url, call_func, redirect_times, errors+1)
170
+ else
171
+ response
172
+ end
173
+ end
174
+
175
+ def make_call_sleep_seconds
176
+ 2
117
177
  end
118
178
 
119
179
  def osx?
@@ -126,7 +186,7 @@ module Mortar
126
186
  end
127
187
 
128
188
  def url_date(url)
129
- result = Excon.head(url)
189
+ result = head_resource(url)
130
190
  http_date_to_epoch(result.get_header('Last-Modified'))
131
191
  end
132
192
 
@@ -21,7 +21,7 @@ class Mortar::Local::Jython
21
21
 
22
22
  JYTHON_VERSION = '2.5.2'
23
23
  JYTHON_JAR_NAME = 'jython_installer-' + JYTHON_VERSION + '.jar'
24
- JYTHON_JAR_DIR = "http://s3.amazonaws.com/hawk-dev-software-mirror/jython/jython-2.5.2/"
24
+ JYTHON_JAR_DEFAULT_URL_PATH = "resource/jython"
25
25
 
26
26
  def install_or_update
27
27
  if should_install
@@ -40,24 +40,30 @@ class Mortar::Local::Jython
40
40
  end
41
41
 
42
42
  def install
43
- unless File.exists?(local_install_directory + '/' + JYTHON_JAR_NAME)
44
- download_file(JYTHON_JAR_DIR + JYTHON_JAR_NAME, local_install_directory)
43
+ jython_file = File.join(local_install_directory, JYTHON_JAR_NAME)
44
+ unless File.exists?(jython_file)
45
+ download_file(jython_jar_url, jython_file)
45
46
  end
46
47
 
47
48
  `$JAVA_HOME/bin/java -jar #{local_install_directory + '/' + JYTHON_JAR_NAME} -s -d #{jython_directory}`
48
49
  FileUtils.mkdir_p jython_cache_directory
49
50
  FileUtils.chmod_R 0777, jython_cache_directory
50
51
 
51
- FileUtils.rm(local_install_directory + '/' + JYTHON_JAR_NAME)
52
+ FileUtils.rm(jython_file)
52
53
  note_install('jython')
53
54
  end
54
55
 
55
56
  def should_update
56
- return is_newer_version('jython', JYTHON_JAR_DIR + JYTHON_JAR_NAME)
57
+ return is_newer_version('jython', jython_jar_url)
57
58
  end
58
59
 
59
60
  def update
60
61
  FileUtils.rm_r(jython_directory)
61
62
  install
62
63
  end
64
+
65
+ def jython_jar_url
66
+ default_url = host + "/" + JYTHON_JAR_DEFAULT_URL_PATH
67
+ ENV.fetch('JYTHON_JAR_URL', default_url)
68
+ end
63
69
  end
@@ -23,7 +23,8 @@ class Mortar::Local::Pig
23
23
  include Mortar::Local::InstallUtil
24
24
 
25
25
  PIG_LOG_FORMAT = "humanreadable"
26
- PIG_TAR_DEFAULT_URL = "https://s3.amazonaws.com/mhc-software-mirror/cli/pig.tgz"
26
+ PIG_TGZ_NAME = "pig.tgz"
27
+ PIG_TGZ_DEFAULT_URL_PATH = "resource/pig"
27
28
 
28
29
  # Tempfile objects have a hook to delete the file when the object is
29
30
  # destroyed by the garbage collector. In practice this means that a
@@ -77,11 +78,8 @@ class Mortar::Local::Pig
77
78
  end
78
79
 
79
80
  def pig_archive_url
80
- ENV.fetch('PIG_DISTRO_URL', PIG_TAR_DEFAULT_URL)
81
- end
82
-
83
- def pig_archive_file
84
- File.basename(pig_archive_url)
81
+ default_url = host + "/" + PIG_TGZ_DEFAULT_URL_PATH
82
+ ENV.fetch('PIG_DISTRO_URL', default_url)
85
83
  end
86
84
 
87
85
  # Determines if a pig install needs to occur, true if no pig install present
@@ -96,7 +94,6 @@ class Mortar::Local::Pig
96
94
  end
97
95
 
98
96
  def install_or_update()
99
- call_install = false
100
97
  if should_do_pig_install?
101
98
  action "Installing pig to #{local_install_directory_name}" do
102
99
  install()
@@ -111,8 +108,8 @@ class Mortar::Local::Pig
111
108
  # Installs pig for this project if it is not already present
112
109
  def install
113
110
  FileUtils.mkdir_p(local_install_directory)
114
- download_file(pig_archive_url, local_install_directory)
115
- local_tgz = File.join(local_install_directory, pig_archive_file)
111
+ local_tgz = File.join(local_install_directory, PIG_TGZ_NAME)
112
+ download_file(pig_archive_url, local_tgz)
116
113
  extract_tgz(local_tgz, local_install_directory)
117
114
 
118
115
  # This has been seening coming out of the tgz w/o +x so we do
@@ -19,13 +19,16 @@ require "mortar/local/installutil"
19
19
  class Mortar::Local::Python
20
20
  include Mortar::Local::InstallUtil
21
21
 
22
- PYTHON_DEFAULT_TGZ_URL = "https://s3.amazonaws.com/mhc-software-mirror/cli/mortar-python-osx.tgz"
22
+ PYTHON_OSX_TGZ_NAME = "mortar-python-osx.tgz"
23
+ PYTHON_OSX_TGZ_DEFAULT_URL_PATH = "resource/python_osx"
23
24
 
24
25
  # Path to the python binary that should be used
25
26
  # for running UDFs
26
27
  @command = nil
27
28
 
28
29
 
30
+ @candidate_pythons = nil
31
+
29
32
  # Execute either an installation of python or an inspection
30
33
  # of the local system to see if a usable python is available
31
34
  def check_or_install
@@ -38,6 +41,17 @@ class Mortar::Local::Python
38
41
  end
39
42
  end
40
43
 
44
+ def check_virtualenv
45
+ # Assumes you've already called check_or_install(), in which case
46
+ # we can skip osx as its installation includeds virtualenv
47
+ if osx?
48
+ return true
49
+ else
50
+ return check_pythons_for_virtenv
51
+ end
52
+
53
+ end
54
+
41
55
  def should_do_update?
42
56
  return is_newer_version('python', python_archive_url)
43
57
  end
@@ -60,13 +74,14 @@ class Mortar::Local::Python
60
74
 
61
75
  def install_osx
62
76
  FileUtils.mkdir_p(local_install_directory)
63
- download_file(python_archive_url, local_install_directory)
64
- extract_tgz(local_install_directory + "/" + python_archive_file, local_install_directory)
77
+ python_tgz_path = File.join(local_install_directory, PYTHON_OSX_TGZ_NAME)
78
+ download_file(python_archive_url, python_tgz_path)
79
+ extract_tgz(python_tgz_path, local_install_directory)
65
80
 
66
81
  # This has been seening coming out of the tgz w/o +x so we do
67
82
  # here to be sure it has the necessary permissions
68
83
  FileUtils.chmod(0755, @command)
69
- File.delete(local_install_directory + "/" + python_archive_file)
84
+ File.delete(python_tgz_path)
70
85
  note_install("python")
71
86
  end
72
87
 
@@ -76,21 +91,31 @@ class Mortar::Local::Python
76
91
  return (osx? and (not (File.exists?(python_directory))))
77
92
  end
78
93
 
94
+ def candidates
95
+ @candidate_pythons.dup
96
+ end
79
97
 
80
98
  # Checks if there is a usable versionpython already installed
81
99
  def check_system_python
82
- py_cmd = path_to_local_python()
83
- if not py_cmd
84
- false
85
- else
86
- @command = py_cmd
87
- true
88
- end
100
+ @candidate_pythons = lookup_local_pythons
101
+ return 0 != @candidate_pythons.length
102
+ end
103
+
104
+ # Inspects the list of found python installations and
105
+ # checks if they have virtualenv installed. The first
106
+ # one found will be used.
107
+ def check_pythons_for_virtenv
108
+ @candidate_pythons.each{ |py|
109
+ if has_virtualenv_installed(py)
110
+ @command = py
111
+ true
112
+ end
113
+ }
89
114
  end
90
115
 
91
116
  # Checks if the specified python command has
92
117
  # virtualenv installed
93
- def check_virtualenv_installed(python)
118
+ def has_virtualenv_installed(python)
94
119
  `#{python} -m virtualenv --help`
95
120
  if (0 != $?.to_i)
96
121
  false
@@ -99,18 +124,16 @@ class Mortar::Local::Python
99
124
  end
100
125
  end
101
126
 
102
- def path_to_local_python
127
+ def lookup_local_pythons
103
128
  # Check several python commands in decending level of desirability
129
+ found_bins = []
104
130
  [ "python#{desired_python_minor_version}", "python" ].each{ |cmd|
105
131
  path_to_python = `which #{cmd}`.to_s.strip
106
132
  if path_to_python != ''
107
- # todo: check for a minimum version (in the case of 'python')
108
- if check_virtualenv_installed(path_to_python)
109
- return path_to_python
110
- end
133
+ found_bins << path_to_python
111
134
  end
112
135
  }
113
- return nil
136
+ return found_bins
114
137
  end
115
138
 
116
139
 
@@ -135,11 +158,8 @@ class Mortar::Local::Python
135
158
  end
136
159
 
137
160
  def python_archive_url
138
- return ENV.fetch('PYTHON_DISTRO_URL', PYTHON_DEFAULT_TGZ_URL)
139
- end
140
-
141
- def python_archive_file
142
- File.basename(python_archive_url)
161
+ default_url = host + "/" + PYTHON_OSX_TGZ_DEFAULT_URL_PATH
162
+ return ENV.fetch('PYTHON_DISTRO_URL', default_url)
143
163
  end
144
164
 
145
165
  # Creates a virtualenv in a well known location and installs any packages