mortar 0.9.3 → 0.9.4

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.
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