
    gS                     X   d Z ddlmZ ddlmZ ddlmZ ddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlmZ ddlmZ ddlmZ dd	lmZ dd
lmZ ddlmZ ddlZddlmZ dZ G d dej4                        Z G d de      Z G d de      Z G d de      Z G d de      Z G d de      Z  G d de      Z! G d de      Z"d Z#d Z$ejJ                  d         Z& G d! d" ejN                  ejP                  e)            Z* G d# d$e*      Z+ G d% d&e*      Z,d' Z-d( Z.d) Z/d* Z0d-d+Z1d.d,Z2y)/zUtilities for job submission preparation.

The main entry point is UploadPythonPackages, which takes in parameters derived
from the command line arguments and returns a list of URLs to be given to the
AI Platform API. See its docstring for details.
    )absolute_import)division)unicode_literalsN)storage_util)uploads)
exceptions)execution_utils)log)files)mapzfrom setuptools import setup, find_packages

if __name__ == '__main__':
    setup(
        name='{package_name}',
        packages=find_packages(include=['{package_name}'])
    )
c                       e Zd ZdZy)UploadFailureErrorz0Generic error with the packaging/upload process.N__name__
__module____qualname____doc__     5lib/googlecloudsdk/command_lib/ml_engine/jobs_prep.pyr   r   7   s    8r   r   c                   "     e Zd ZdZ fdZ xZS )SetuptoolsFailedErrorz/Error indicating that setuptools itself failed.c                 h    dj                  |      }|r|dz  }n|dz  }t        t        |   |       y )Nz8Packaging of user Python code failed with message:

{}

zTTry manually writing a setup.py file at your package root and rerunning the command.zTry manually building your Python code by running:
  $ python setup.py sdist
and providing the output via the `--packages` flag (for example, `--packages dist/package.tar.gz,dist/package2.whl)`)formatsuperr   __init__)selfoutput	generatedmsg	__class__s       r   r   zSetuptoolsFailedError.__init__?   sM    VF^ 	 ' (c 
 M Nc 

/4r   r   r   r   r   r   __classcell__r!   s   @r   r   r   <   s    75 5r   r   c                   "     e Zd ZdZ fdZ xZS )SysExecutableMissingErrorz/Error indicating that sys.executable was empty.c                 R    t         t        |   t        j                  d             y )Nz        No Python executable found on path. A Python executable with setuptools
        installed on the PYTHONPATH is required for building AI Platform training jobs.
        )r   r&   r   textwrapdedent)r   r!   s    r   r   z"SysExecutableMissingError.__init__P   s$    	
#T3  	r   r"   r$   s   @r   r&   r&   M   s    7 r   r&   c                   "     e Zd ZdZ fdZ xZS )MissingInitErrorzCError indicating that the package to build had no __init__.py file.c                 p    t         t        |   t        j                  d      j                  |             y )Nz        [{}] is not a valid Python package because it does not contain an         `__init__.py` file. Please create one and try again. Also, please         ensure that --package-path refers to a local directory.
        )r   r+   r   r(   r)   r   )r   package_dirr!   s     r   r   zMissingInitError.__init__[   s/    	
D*8?? < , VK 	"r   r"   r$   s   @r   r+   r+   X   s    K" "r   r+   c                       e Zd ZdZy)UncopyablePackageErrorzError with copying the package.Nr   r   r   r   r/   r/   c   s    'r   r/   c                   "     e Zd ZdZ fdZ xZS )DuplicateEntriesErrorzFError indicating that multiple files with the same name were provided.c                 h    t         t        |   dj                  dj	                  |                   y )Nz<Cannot upload multiple packages with the same filename: [{}], )r   r1   r   r   join)r   
duplicatesr!   s     r   r   zDuplicateEntriesError.__init__j   s-    	
/FMMIIj!	#$r   r"   r$   s   @r   r1   r1   g   s    N$ $r   r1   c                       e Zd ZdZy)NoStagingLocationErrorz6No staging location was provided but one was required.Nr   r   r   r   r7   r7   p   s    >r   r7   c                   "     e Zd ZdZ fdZ xZS )InvalidSourceDirErrorz6Error indicating that the source directory is invalid.c                 J    t         t        |   dj                  |             y )Nz/Source directory [{}] is not a valid directory.)r   r9   r   r   )r   
source_dirr!   s     r   r   zInvalidSourceDirError.__init__w   s"    	
/9@@LNr   r"   r$   s   @r   r9   r9   t   s    >N Nr   r9   c                 
   t         j                  j                  |       st        |       	 t	        j
                  |       }|r| S t	        j                  | |      rt        dj                  ||             t         j                  j                  |d      }t        j                  d| |       	 t	        j                  | |       |S # t        $ r t        |       w xY w# t        $ r t        dj                  |            w xY w)ar  Returns a writable directory with the same contents as source_dir.

  If source_dir is writable, it is used. Otherwise, a directory 'dest' inside of
  temp_dir is used.

  Args:
    source_dir: str, the directory to (potentially) copy
    temp_dir: str, the path to a writable temporary directory in which to store
      any copied code.

  Returns:
    str, the path to a writable directory with the same contents as source_dir
      (i.e. source_dir, if it's writable, or a copy otherwise).

  Raises:
    UploadFailureError: if the command exits non-zero.
    InvalidSourceDirError: if the source directory is not valid.
  zVCannot copy directory since working directory [{}] is inside of source directory [{}].destz+Copying local source tree from [%s] to [%s]z%Cannot write to working location [{}])ospathisdirr9   r   HasWriteAccessInDir
ValueErrorIsDirAncestorOfr/   r   r4   r
   debugCopyTreeOSError)r;   temp_dirwritabledest_dirs       r   _CopyIfNotWritablerJ   |   s    & 
z	"


++,((4H 
:x0
 	!!'*!=? ? WW\\(F+())9:xPB	NN:x( 
/% 
 ,


++, 
 B
 /66x@B BBs   C .C C$Dc                     t        j                  d|        t        j                  j	                  |       rt        j
                  d|        yt        j                  |      }t        j
                  d|       t        j                  | |       y)aZ  Generates a temporary setup.py file if there is none at the given path.

  Args:
    setup_py_path: str, a path to the expected setup.py location.
    package_name: str, the name of the Python package for which to write a
      setup.py file (used in the generated file contents).

  Returns:
    bool, whether the setup.py file was generated.
  z!Looking for setup.py file at [%s]z$Using existing setup.py file at [%s]F)package_namez&Generating temporary setup.py file:
%sT)
r
   rD   r>   r?   isfileinfoDEFAULT_SETUP_FILEr   r   WriteFileContents)setup_py_pathrL   setup_contentss      r   _GenerateSetupPyIfNeededrS      sj     ))/?WW^^M"HH3]C%,,,,G.((4nE-8	r   c              #      K   	 t        j                         }|j                         }	 | |r" |j                  t        j                           yy# t        $ r d}| }Y 9w xY w# |r" |j                  t        j                           w w xY ww)a  Yields a temporary directory or a backup temporary directory.

  Prefers creating a temporary directory (which will be cleaned up when the
  context manager is closed), but falls back to default_dir. There are systems
  where users can't write to temp, but we still need to copy.

  Args:
    default_dir: str, the backup temporary directory.

  Yields:
    str, the temporary directory.
  N)r   TemporaryDirectory	__enter__rF   __exit__sysexc_info)default_dirrG   r?   s      r   _TempDirOrBackupr[      s     
'')H
 D)
Jh(  
 HD h( s8   B$A A% %BA"B!A""B%&BBc                   d    e Zd ZdZd Zej                  d        Zej                  d        Zd Z	y)_SetupPyCommanda  A command to run setup.py in a given environment.

  Includes the Python version to use and the arguments with which to run
  setup.py.

  Attributes:
    setup_py_path: str, the path to the setup.py file
    setup_py_args: list of str, the arguments with which to call setup.py
    package_root: str, path to the directory containing the package to build
      (must be writable, or setuptools will fail)
  c                 .    || _         || _        || _        y N)rQ   setup_py_argspackage_root)r   rQ   r`   ra   s       r   r   z_SetupPyCommand.__init__   s    &D&D$Dr   c                     t               )zEReturns arguments to use for execution (including Python executable).NotImplementedErrorr   s    r   GetArgsz_SetupPyCommand.GetArgs        
r   c                     t               )z?Returns the environment dictionary to use for Python execution.rc   re   s    r   GetEnvz_SetupPyCommand.GetEnv   rg   r   c           	          t        j                  | j                         d|j                  |j                  | j                  | j                               S )zRun the configured setup.py command.

    Args:
      out: a stream to which the command output should be written.

    Returns:
      int, the return code of the command.
    T)no_exitout_funcerr_funccwdenv)r	   Execrf   writera   ri   )r   outs     r   Executez_SetupPyCommand.Execute   s@     syy3994;;=2 2r   N)
r   r   r   r   r   abcabstractmethodrf   ri   rs   r   r   r   r]   r]      sE    
%
        2r   r]   c                       e Zd ZdZd Zd Zy)_CloudSdkPythonSetupPyCommandzA command that uses the Cloud SDK Python environment.

  It uses the same OS environment, plus the same PYTHONPATH.

  This is preferred, since it's more controlled.
  c                 l    t        j                  | j                  g| j                  dt	               iS )Npython)r	   ArgsForPythonToolrQ   r`   GetPythonExecutablere   s    r   rf   z%_CloudSdkPythonSetupPyCommand.GetArgs  s:    ,,T-?-? K.2.@.@K4G4IK Kr   c                     t         j                  j                         }t         j                  j	                  t
        j                        |d<   |S )N
PYTHONPATH)r>   environcopypathsepr4   rX   r?   )r   exec_envs     r   ri   z$_CloudSdkPythonSetupPyCommand.GetEnv  s2    zz HZZ__SXX6H\Or   Nr   r   r   r   rf   ri   r   r   r   rw   rw     s    K
r   rw   c                       e Zd ZdZd Zd Zy)_SystemPythonSetupPyCommandzA command that uses the system Python environment.

  Uses the same executable as the Cloud SDK.

  Important in case of e.g. a setup.py file that has non-stdlib dependencies.
  c                 H    t               | j                  g| j                  z   S r_   )r{   rQ   r`   re   s    r   rf   z#_SystemPythonSetupPyCommand.GetArgs'  s!    !4#5#569K9KKKr   c                      y r_   r   re   s    r   ri   z"_SystemPythonSetupPyCommand.GetEnv*  s    r   Nr   r   r   r   r   r     s    Lr   r   c                  b    d } 	 t        j                         } | S # t        $ r t               w xY wr_   )r	   r{   rB   r&   )python_executables    r   r{   r{   .  s>    &';;= 
 
 &
#
%%&s    .c           	      n   t        |       5 }dd|g}dd|d|g}dd|g}||z   |z   ||z   |f}g }|D ]:  }	|j                  t        ||	|              |j                  t        ||	|              < |D ]+  }
t	        j
                         }|
j                  |      }|r+ n t        j                               	 ddd       t        j                  |      D cg c]"  }t        j                  j                  ||      $ }}t        j                  d	d
j                  |             |S # 1 sw Y   pxY wc c}w )a  Executes the setuptools `sdist` command.

  Specifically, runs `python setup.py sdist` (with the full path to `setup.py`
  given by setup_py_path) with arguments to put the final output in output_dir
  and all possible temporary files in a temporary directory. package_root is
  used as the working directory.

  May attempt to run setup.py multiple times with different
  environments/commands if any execution fails:

  1. Using the Cloud SDK Python environment, with a full setuptools invocation
     (`egg_info`, `build`, and `sdist`).
  2. Using the system Python environment, with a full setuptools invocation
     (`egg_info`, `build`, and `sdist`).
  3. Using the Cloud SDK Python environment, with an intermediate setuptools
     invocation (`build` and `sdist`).
  4. Using the system Python environment, with an intermediate setuptools
     invocation (`build` and `sdist`).
  5. Using the Cloud SDK Python environment, with a simple setuptools
     invocation which will also work for plain distutils-based setup.py (just
     `sdist`).
  6. Using the system Python environment, with a simple setuptools
     invocation which will also work for plain distutils-based setup.py (just
     `sdist`).

  The reason for this order is that it prefers first the setup.py invocations
  which leave the fewest files on disk. Then, we prefer the Cloud SDK execution
  environment as it will be the most stable.

  package_root must be writable, or setuptools will fail (there are
  temporary files from setuptools that get put in the CWD).

  Args:
    package_root: str, the directory containing the package (that is, the
      *parent* of the package itself).
    setup_py_path: str, the path to the `setup.py` file to execute.
    output_dir: str, path to a directory in which the built packages should be
      created.

  Returns:
    list of str, the full paths to the generated packages.

  Raises:
    SysExecutableMissingError: if sys.executable is None
    RuntimeError: if the execution of setuptools exited non-zero.
  sdistz
--dist-dirbuildz--build-basez--build-tempegg_infoz
--egg-baseNz!Python packaging resulted in [%s]r3   )r[   appendrw   r   ioStringIOrs   RuntimeErrorgetvaluer>   listdirr?   r4   r
   rD   )ra   rQ   
output_dirworking_dir
sdist_args
build_argsegg_info_argssetup_py_arg_setssetup_py_commandsr`   setup_py_commandrr   return_coderel_filelocal_pathss                  r   _RunSetupToolsr   7  s_   d % <4J
 	nkKJ  {;M
"Z/Z *<
 6 7:
 6 7 + .KKMc$,,S1k	 . (( 	; &D "$J!79!7X j(3!7  9))/;1GH	I &%B9s   BD&D&'D2&D/c           	         t         j                  j                  |       } t         j                  j                  |       }t	        |       5 }t        ||      }t         j                  j                  t         j                  j                  | d            st        |       t         j                  j                  |d      }t         j                  j                  |       }t        ||      }	 t        |||      |r?t         j                  j                  |d      }||fD ]  }	 t        j                  |        cddd       S # t        $ r t        j                  d|       Y Fw xY w# t         $ r$}	t#        t%        j&                  |	      |      d}	~	ww xY w# |ret         j                  j                  |d      }||fD ]=  }	 t        j                  |       # t        $ r t        j                  d|       Y ;w xY w w w xY w# 1 sw Y   yxY w)az  Builds Python packages from the given package source.

  That is, builds Python packages from the code in package_path, using its
  parent directory (the 'package root') as its context using the setuptools
  `sdist` command.

  If there is a `setup.py` file in the package root, use that. Otherwise,
  use a simple, temporary one made for this package.

  We try to be as unobstrustive as possible (see _RunSetupTools for details):

  - setuptools writes some files to the package root--we move as many temporary
    generated files out of the package root as possible
  - the final output gets written to output_dir
  - any temporary setup.py file is written outside of the package root.
  - if the current directory isn't writable, we silenly make a temporary copy

  Args:
    package_path: str. Path to the package. This should be the path to
      the directory containing the Python code to be built, *not* its parent
      (which optionally contains setup.py and other metadata).
    output_dir: str, path to a long-lived directory in which the built packages
      should be created.

  Returns:
    list of str. The full local path to all built Python packages.

  Raises:
    SetuptoolsFailedError: If the setup.py file fails to successfully build.
    MissingInitError: If the package doesn't contain an `__init__.py` file.
    InvalidSourceDirError: if the source directory is not valid.
  z__init__.pyzsetup.pyz	setup.pycz;Couldn't remove file [%s] (it may never have been created).N)r>   r?   abspathdirnamer[   rJ   existsr4   r+   basenamerS   r   unlinkrF   r
   rD   r   r   six	text_type)
package_pathr   ra   r   rQ   rL   r   pyc_filer?   errs
             r   BuildPackagesr     s   B .,.,%%lK@L77>>"'',,|]CD \**GGLLz:M77##L1L(EIL-D 

 77<<k:"H-DIIdO .3 &%8  IIM  A!#--"4i@@A 

 77<<k:"H-DIIdO IIM . 
' &%s   
B H+E'7)H!E6HE$	!H#E$	$H'	F0FFF*H GH G:7H 9G::H  HHc                    |s
t               t        j                  t        t	        t
        j                  j                  |                   }t        j                  |      D cg c]  \  }}|dkD  s| }}}|rt        |      | D cg c]#  }|t
        j                  j                  |      f% }}t        j                  ||j                  |j                        S c c}}w c c}w )z;Uploads files after validating and transforming input type.   )r7   collectionsCounterlistr   r>   r?   r   r   	iteritemsr1   r   UploadFiles
bucket_refname)pathsstaging_locationcounterr   countr5   r?   upload_pairss           r   _UploadFilesByPathr     s    	
 
""S)9)95%A BC'(+g(>L(>u%!)(>*L


++=BCUT4))$/0U,C			\+;+F+F-22
4 4 M Ds   %C 3C (C&c           
      D   g }g }| D ]D  }t         j                  j                  |      r|j                  |       4|j                  |       F |rt        j
                  j                  t        j
                  j                  |            }t        |      5 }|j                  t        |t        j
                  j                  |d                   |j                  t        ||             ddd       |S |r|j                  t        ||             |S # 1 sw Y   |S xY w)a  Uploads Python packages (if necessary), building them as-specified.

  An AI Platform job needs one or more Python packages to run. These Python
  packages can be specified in one of three ways:

    1. As a path to a local, pre-built Python package file.
    2. As a path to a Cloud Storage-hosted, pre-built Python package file (paths
       beginning with 'gs://').
    3. As a local Python source tree (the `--package-path` flag).

  In case 1, we upload the local files to Cloud Storage[1] and provide their
  paths. These can then be given to the AI Platform API, which can fetch
  these files.

  In case 2, we don't need to do anything. We can just send these paths directly
  to the AI Platform API.

  In case 3, we perform a build using setuptools[2], and upload the resulting
  artifacts to Cloud Storage[1]. The paths to these artifacts can be given to
  the AI Platform API. See the `BuildPackages` method.

  These methods of specifying Python packages may be combined.


  [1] Uploads are to a specially-prefixed location in a user-provided Cloud
  Storage staging bucket. If the user provides bucket `gs://my-bucket/`, a file
  `package.tar.gz` is uploaded to
  `gs://my-bucket/<job name>/<checksum>/package.tar.gz`.

  [2] setuptools must be installed on the local user system.

  Args:
    packages: list of str. Path to extra tar.gz packages to upload, if any. If
      empty, a package_path must be provided.
    package_path: str. Relative path to source directory to be built, if any. If
      omitted, one or more packages must be provided.
    staging_location: storage_util.ObjectReference. Cloud Storage prefix to
      which archives are uploaded. Not necessary if only remote packages are
      given.

  Returns:
    list of str. Fully qualified Cloud Storage URLs (`gs://..`) from uploaded
      packages.

  Raises:
    ValueError: If packages is empty, and building package_path produces no
      tar archives.
    SetuptoolsFailedError: If the setup.py file fails to successfully build.
    MissingInitError: If the package doesn't contain an `__init__.py` file.
    DuplicateEntriesError: If multiple files with the same name were provided.
    ArgumentError: if no packages were found in the given path or no
      staging_location was but uploads were required.
  r   N)r   ObjectReferenceIsStorageUrlr   r>   r?   r   r   r[   extendr   r4   r   )packagesr   r   remote_pathsr   packagera   r   s           r   UploadPythonPackagesr     s    l ,+g##009'"!	  77??277??<#@AL	,	';|')ww||K'JL M,[:JKL 
( 
  *;8HIJ	 
( 
s   ADDc           	      (   d}|r"t         j                  j                  ||       }|S |rdt         j                  j                  |j                  dj                  |j                  j                  d      dfD cg c]  }|r| c}            }|S c c}w )zEGet the appropriate staging location for the job given the arguments.N/r   )r   r   FromBucketRefFromNamebucketr4   r   rstrip)job_idstaging_bucketjob_dirr   fs        r   GetStagingLocationr   /  s    #33AA  
	 #33<<gll.A.A#.F.8.: "@ .:=> #$ .: "@ AB 
"@s   7B)r   NN)NNN)3r   
__future__r   r   r   rt   r   
contextlibr   r>   rX   r(   googlecloudsdk.api_lib.storager   $googlecloudsdk.command_lib.ml_enginer   googlecloudsdk.corer   r	   r
   googlecloudsdk.core.utilr   r   	six.movesr   rO   Errorr   r   r&   r+   r/   r1   r7   r9   rJ   rS   contextmanagerr[   with_metaclassABCMetaobjectr]   rw   r   r{   r   r   r   r   r   r   r   r   <module>r      s<   '  ' 
   	 	 
  7 8 * / # * 
  )) 
5. 5" 2 ") "(/ ($. $?/ ?N. N+\, ) )@(2(c((f= (2VO &/ VrBJ4IX
r   