@@ -68,6 +68,7 @@ def __init__(self, *args, **kwargs) -> None:
6868 quarto_inspection = kwargs .get ("quarto_inspection" )
6969 environment = kwargs .get ("environment" )
7070 image = kwargs .get ("image" )
71+ primary_html = kwargs .get ("primary_html" )
7172
7273 self .data ["version" ] = version if version else 1
7374 if environment :
@@ -82,6 +83,8 @@ def __init__(self, *args, **kwargs) -> None:
8283 "appmode" : AppModes .UNKNOWN ,
8384 }
8485 )
86+ if primary_html :
87+ self .data ["metadata" ]["primary_html" ] = primary_html
8588
8689 if entrypoint :
8790 self .data ["metadata" ]["entrypoint" ] = entrypoint
@@ -150,6 +153,18 @@ def entrypoint(self):
150153 def entrypoint (self , value ):
151154 self .data ["metadata" ]["entrypoint" ] = value
152155
156+ @property
157+ def primary_html (self ):
158+ if "metadata" not in self .data :
159+ return None
160+ if "primary_html" in self .data ["metadata" ]:
161+ return self .data ["metadata" ]["primary_html" ]
162+ return None
163+
164+ @primary_html .setter
165+ def primary_html (self , value ):
166+ self .data ["metadata" ]["primary_html" ] = value
167+
153168 def add_file (self , path ):
154169 self .data ["files" ][path ] = {"checksum" : file_checksum (path )}
155170 return self
@@ -207,6 +222,12 @@ def flattened_entrypoint(self):
207222 raise RSConnectException ("A valid entrypoint must be provided." )
208223 return relpath (self .entrypoint , dirname (self .entrypoint ))
209224
225+ @property
226+ def flattened_primary_html (self ):
227+ if self .primary_html is None :
228+ raise RSConnectException ("A valid primary_html must be provided." )
229+ return relpath (self .primary_html , dirname (self .primary_html ))
230+
210231 @property
211232 def flattened_copy (self ):
212233 if self .entrypoint is None :
@@ -215,6 +236,8 @@ def flattened_copy(self):
215236 new_manifest .data ["files" ] = self .flattened_data
216237 new_manifest .buffer = self .flattened_buffer
217238 new_manifest .entrypoint = self .flattened_entrypoint
239+ if self .primary_html :
240+ new_manifest .primary_html = self .flattened_primary_html
218241 return new_manifest
219242
220243 def make_relative_to_deploy_dir (self ):
@@ -817,61 +840,111 @@ def make_api_manifest(
817840 return manifest , relevant_files
818841
819842
820- def make_html_bundle_content (
843+ def create_html_manifest (
821844 path : str ,
822845 entrypoint : str ,
823- extra_files : typing .List [str ],
824- excludes : typing .List [str ],
846+ extra_files : typing .List [str ] = None ,
847+ excludes : typing .List [str ] = None ,
825848 image : str = None ,
826- ) -> typing . Tuple [ typing . Dict [ str , typing . Any ], typing . List [ str ]]:
827-
849+ ** kwargs
850+ ) -> Manifest :
828851 """
829- Makes a manifest for static html deployment .
852+ Creates and writes a manifest.json file for the given path .
830853
831854 :param path: the file, or the directory containing the files to deploy.
832855 :param entry_point: the main entry point for the API.
833- :param extra_files: a sequence of any extra files to include in the bundle.
856+ :param environment: the Python environment to start with. This should be what's
857+ returned by the inspect_environment() function.
858+ :param app_mode: the application mode to assume. If this is None, the extension
859+ portion of the entry point file name will be used to derive one. Previous default = None.
860+ :param extra_files: any extra files that should be included in the manifest. Previous default = None.
834861 :param excludes: a sequence of glob patterns that will exclude matched files.
862+ :param force_generate: bool indicating whether to force generate manifest and related environment files.
835863 :param image: the optional docker image to be specified for off-host execution. Default = None.
836- :return: the manifest and a list of the files involved .
864+ :return: the manifest data structure .
837865 """
866+ if not path :
867+ raise RSConnectException ("A valid path must be provided." )
838868 extra_files = list (extra_files ) if extra_files else []
839- entrypoint = entrypoint or infer_entrypoint (path = path , mimetype = "text/html" )
840- if not entrypoint :
841- raise RSConnectException ("Unable to find a valid html entry point." )
869+ entrypoint_candidates = infer_entrypoint_candidates (path = abspath (path ), mimetype = "text/html" )
842870
843- if path .startswith (os .curdir ):
844- path = relpath (path )
845- if entrypoint .startswith (os .curdir ):
846- entrypoint = relpath (entrypoint )
847- extra_files = [relpath (f ) if isfile (f ) and f .startswith (os .curdir ) else f for f in extra_files ]
871+ deploy_dir = guess_deploy_dir (path , entrypoint )
872+ if len (entrypoint_candidates ) <= 0 :
873+ if entrypoint is None :
874+ raise RSConnectException ("No valid entrypoint found." )
875+ entrypoint = abs_entrypoint (path , entrypoint )
876+ elif len (entrypoint_candidates ) == 1 :
877+ if entrypoint :
878+ entrypoint = abs_entrypoint (path , entrypoint )
879+ else :
880+ entrypoint = entrypoint_candidates [0 ]
881+ else : # len(entrypoint_candidates) > 1:
882+ if entrypoint is None :
883+ raise RSConnectException ("No valid entrypoint found." )
884+ entrypoint = abs_entrypoint (path , entrypoint )
848885
849- if is_environment_dir (path ):
850- excludes = list (excludes or []) + ["bin/" , "lib/" ]
886+ extra_files = validate_extra_files (deploy_dir , extra_files , use_abspath = True )
887+ excludes = list (excludes ) if excludes else []
888+ excludes .extend (["manifest.json" ])
889+ excludes .extend (list_environment_dirs (deploy_dir ))
851890
852- extra_files = extra_files or []
853- skip = ["manifest.json" ]
854- extra_files = sorted (set (extra_files ) - set (skip ))
891+ manifest = Manifest (app_mode = AppModes .STATIC , entrypoint = entrypoint , primary_html = entrypoint , image = image )
892+ manifest .deploy_dir = deploy_dir
855893
856- # Don't include these top-level files.
857- excludes = list (excludes ) if excludes else []
858- excludes .append ("manifest.json" )
859- if not isfile (path ):
860- excludes .extend (list_environment_dirs (path ))
894+ file_list = create_file_list (path , extra_files , excludes , use_abspath = True )
895+ for abs_path in file_list :
896+ manifest .add_file (abs_path )
861897
862- relevant_files = create_file_list (path , extra_files , excludes )
863- manifest = make_html_manifest (entrypoint , image )
898+ return manifest
864899
865- for rel_path in relevant_files :
866- manifest_add_file (manifest , rel_path , path )
867900
868- return manifest , relevant_files
901+ def make_html_bundle (
902+ path : str ,
903+ entrypoint : str ,
904+ extra_files : typing .List [str ],
905+ excludes : typing .List [str ],
906+ image : str = None ,
907+ ) -> typing .IO [bytes ]:
908+ """
909+ Create an html bundle, given a path and/or entrypoint.
910+
911+ The bundle contains a manifest.json file created for the given notebook entrypoint file.
912+ If the related environment file (requirements.txt) doesn't
913+ exist (or force_generate is set to True), the environment file will also be written.
914+
915+ :param path: the file, or the directory containing the files to deploy.
916+ :param entry_point: the main entry point.
917+ :param extra_files: a sequence of any extra files to include in the bundle.
918+ :param excludes: a sequence of glob patterns that will exclude matched files.
919+ :param force_generate: bool indicating whether to force generate manifest and related environment files.
920+ :param image: the optional docker image to be specified for off-host execution. Default = None.
921+ :return: a file-like object containing the bundle tarball.
922+ """
923+
924+ manifest = create_html_manifest (** locals ())
925+ if manifest .data .get ("files" ) is None :
926+ raise RSConnectException ("No valid files were found for the manifest." )
927+
928+ bundle = Bundle ()
929+ for f in manifest .data ["files" ]:
930+ if f in manifest .buffer :
931+ continue
932+ bundle .add_file (f )
933+ for k , v in manifest .flattened_buffer .items ():
934+ bundle .add_to_buffer (k , v )
935+
936+ manifest_flattened_copy_data = manifest .flattened_copy .data
937+ bundle .add_to_buffer ("manifest.json" , json .dumps (manifest_flattened_copy_data , indent = 2 ))
938+ bundle .deploy_dir = manifest .deploy_dir
939+
940+ return bundle .to_file ()
869941
870942
871943def create_file_list (
872944 path : str ,
873945 extra_files : typing .List [str ] = None ,
874946 excludes : typing .List [str ] = None ,
947+ use_abspath : bool = False ,
875948) -> typing .List [str ]:
876949 """
877950 Builds a full list of files under the given path that should be included
@@ -890,7 +963,8 @@ def create_file_list(
890963 file_set = set (extra_files ) # type: typing.Set[str]
891964
892965 if isfile (path ):
893- file_set .add (Path (path ).name )
966+ path_to_add = abspath (path ) if use_abspath else path
967+ file_set .add (path_to_add )
894968 return sorted (file_set )
895969
896970 for cur_dir , sub_dirs , files in os .walk (path ):
@@ -899,15 +973,16 @@ def create_file_list(
899973 if any (parent in exclude_paths for parent in Path (cur_dir ).parents ):
900974 continue
901975 for file in files :
902- abs_path = os .path .join (cur_dir , file )
903- rel_path = relpath (abs_path , path )
976+ cur_path = os .path .join (cur_dir , file )
977+ rel_path = relpath (cur_path , path )
904978
905- if Path (abs_path ) in exclude_paths :
979+ if Path (cur_path ) in exclude_paths :
906980 continue
907981 if keep_manifest_specified_file (rel_path , exclude_paths | directories_to_ignore ) and (
908- rel_path in extra_files or not glob_set .matches (abs_path )
982+ rel_path in extra_files or not glob_set .matches (cur_path )
909983 ):
910- file_set .add (rel_path )
984+ path_to_add = abspath (cur_path ) if use_abspath else rel_path
985+ file_set .add (path_to_add )
911986 return sorted (file_set )
912987
913988
@@ -930,48 +1005,20 @@ def infer_entrypoint_candidates(path, mimetype) -> List:
9301005 mimetype_filelist = defaultdict (list )
9311006
9321007 for file in os .listdir (path ):
933- rel_path = os .path .join (path , file )
934- if not isfile (rel_path ):
1008+ abs_path = os .path .join (path , file )
1009+ if not isfile (abs_path ):
9351010 continue
936- mimetype_filelist [guess_type (file )[0 ]].append (rel_path )
1011+ mimetype_filelist [guess_type (file )[0 ]].append (abs_path )
9371012 if file in default_mimetype_entrypoints [mimetype ]:
938- return file
1013+ return [ abs_path ]
9391014 return mimetype_filelist [mimetype ] or []
9401015
9411016
942- def make_html_bundle (
943- path : str ,
944- entry_point : str ,
945- extra_files : typing .List [str ],
946- excludes : typing .List [str ],
947- image : str = None ,
948- ) -> typing .IO [bytes ]:
949- """
950- Create an html bundle, given a path and a manifest.
951-
952- :param path: the file, or the directory containing the files to deploy.
953- :param entry_point: the main entry point for the API.
954- :param extra_files: a sequence of any extra files to include in the bundle.
955- :param excludes: a sequence of glob patterns that will exclude matched files.
956- :param image: the optional docker image to be specified for off-host execution. Default = None.
957- :return: a file-like object containing the bundle tarball.
958- """
959- manifest , relevant_files = make_html_bundle_content (path , entry_point , extra_files , excludes , image )
960- bundle_file = tempfile .TemporaryFile (prefix = "rsc_bundle" )
961-
962- with tarfile .open (mode = "w:gz" , fileobj = bundle_file ) as bundle :
963- bundle_add_buffer (bundle , "manifest.json" , json .dumps (manifest , indent = 2 ))
964-
965- for rel_path in relevant_files :
966- bundle_add_file (bundle , rel_path , path )
967-
968- # rewind file pointer
969- bundle_file .seek (0 )
970-
971- return bundle_file
972-
973-
9741017def guess_deploy_dir (path , entrypoint ):
1018+ if path and not exists (path ):
1019+ raise RSConnectException (f"Path { path } does not exist." )
1020+ if entrypoint and not exists (entrypoint ):
1021+ raise RSConnectException (f"Entrypoint { entrypoint } does not exist." )
9751022 abs_path = abspath (path ) if path else None
9761023 abs_entrypoint = abspath (entrypoint ) if entrypoint else None
9771024 if not path and not entrypoint :
@@ -1228,7 +1275,7 @@ def validate_file_is_notebook(file_name):
12281275 raise RSConnectException ("A Jupyter notebook (.ipynb) file is required here." )
12291276
12301277
1231- def validate_extra_files (directory , extra_files ):
1278+ def validate_extra_files (directory , extra_files , use_abspath = False ):
12321279 """
12331280 If the user specified a list of extra files, validate that they all exist and are
12341281 beneath the given directory and, if so, return a list of them made relative to that
@@ -1248,6 +1295,7 @@ def validate_extra_files(directory, extra_files):
12481295 raise RSConnectException ("%s must be under %s." % (extra_file , directory ))
12491296 if not exists (join (directory , extra_file )):
12501297 raise RSConnectException ("Could not find file %s under %s" % (extra , directory ))
1298+ extra_file = abspath (join (directory , extra_file )) if use_abspath else extra_file
12511299 result .append (extra_file )
12521300 return result
12531301
@@ -1646,9 +1694,9 @@ def create_voila_manifest(
16461694
16471695 manifest .add_to_buffer (join (deploy_dir , environment .filename ), environment .contents )
16481696
1649- file_list = create_file_list (path , extra_files , excludes )
1650- for rel_path in file_list :
1651- manifest .add_relative_path ( rel_path )
1697+ file_list = create_file_list (path , extra_files , excludes , use_abspath = True )
1698+ for abs_path in file_list :
1699+ manifest .add_file ( abs_path )
16521700 return manifest
16531701
16541702
0 commit comments