@@ -68,6 +68,7 @@ def __init__(self, *args, **kwargs) -> None:
68
68
quarto_inspection = kwargs .get ("quarto_inspection" )
69
69
environment = kwargs .get ("environment" )
70
70
image = kwargs .get ("image" )
71
+ primary_html = kwargs .get ("primary_html" )
71
72
72
73
self .data ["version" ] = version if version else 1
73
74
if environment :
@@ -82,6 +83,8 @@ def __init__(self, *args, **kwargs) -> None:
82
83
"appmode" : AppModes .UNKNOWN ,
83
84
}
84
85
)
86
+ if primary_html :
87
+ self .data ["metadata" ]["primary_html" ] = primary_html
85
88
86
89
if entrypoint :
87
90
self .data ["metadata" ]["entrypoint" ] = entrypoint
@@ -150,6 +153,18 @@ def entrypoint(self):
150
153
def entrypoint (self , value ):
151
154
self .data ["metadata" ]["entrypoint" ] = value
152
155
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
+
153
168
def add_file (self , path ):
154
169
self .data ["files" ][path ] = {"checksum" : file_checksum (path )}
155
170
return self
@@ -207,6 +222,12 @@ def flattened_entrypoint(self):
207
222
raise RSConnectException ("A valid entrypoint must be provided." )
208
223
return relpath (self .entrypoint , dirname (self .entrypoint ))
209
224
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
+
210
231
@property
211
232
def flattened_copy (self ):
212
233
if self .entrypoint is None :
@@ -215,6 +236,8 @@ def flattened_copy(self):
215
236
new_manifest .data ["files" ] = self .flattened_data
216
237
new_manifest .buffer = self .flattened_buffer
217
238
new_manifest .entrypoint = self .flattened_entrypoint
239
+ if self .primary_html :
240
+ new_manifest .primary_html = self .flattened_primary_html
218
241
return new_manifest
219
242
220
243
def make_relative_to_deploy_dir (self ):
@@ -817,61 +840,111 @@ def make_api_manifest(
817
840
return manifest , relevant_files
818
841
819
842
820
- def make_html_bundle_content (
843
+ def create_html_manifest (
821
844
path : str ,
822
845
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 ,
825
848
image : str = None ,
826
- ) -> typing . Tuple [ typing . Dict [ str , typing . Any ], typing . List [ str ]]:
827
-
849
+ ** kwargs
850
+ ) -> Manifest :
828
851
"""
829
- Makes a manifest for static html deployment .
852
+ Creates and writes a manifest.json file for the given path .
830
853
831
854
:param path: the file, or the directory containing the files to deploy.
832
855
: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.
834
861
: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.
835
863
: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 .
837
865
"""
866
+ if not path :
867
+ raise RSConnectException ("A valid path must be provided." )
838
868
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" )
842
870
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 )
848
885
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 ))
851
890
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
855
893
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 )
861
897
862
- relevant_files = create_file_list (path , extra_files , excludes )
863
- manifest = make_html_manifest (entrypoint , image )
898
+ return manifest
864
899
865
- for rel_path in relevant_files :
866
- manifest_add_file (manifest , rel_path , path )
867
900
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 ()
869
941
870
942
871
943
def create_file_list (
872
944
path : str ,
873
945
extra_files : typing .List [str ] = None ,
874
946
excludes : typing .List [str ] = None ,
947
+ use_abspath : bool = False ,
875
948
) -> typing .List [str ]:
876
949
"""
877
950
Builds a full list of files under the given path that should be included
@@ -890,7 +963,8 @@ def create_file_list(
890
963
file_set = set (extra_files ) # type: typing.Set[str]
891
964
892
965
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 )
894
968
return sorted (file_set )
895
969
896
970
for cur_dir , sub_dirs , files in os .walk (path ):
@@ -899,15 +973,16 @@ def create_file_list(
899
973
if any (parent in exclude_paths for parent in Path (cur_dir ).parents ):
900
974
continue
901
975
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 )
904
978
905
- if Path (abs_path ) in exclude_paths :
979
+ if Path (cur_path ) in exclude_paths :
906
980
continue
907
981
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 )
909
983
):
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 )
911
986
return sorted (file_set )
912
987
913
988
@@ -930,48 +1005,20 @@ def infer_entrypoint_candidates(path, mimetype) -> List:
930
1005
mimetype_filelist = defaultdict (list )
931
1006
932
1007
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 ):
935
1010
continue
936
- mimetype_filelist [guess_type (file )[0 ]].append (rel_path )
1011
+ mimetype_filelist [guess_type (file )[0 ]].append (abs_path )
937
1012
if file in default_mimetype_entrypoints [mimetype ]:
938
- return file
1013
+ return [ abs_path ]
939
1014
return mimetype_filelist [mimetype ] or []
940
1015
941
1016
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
-
974
1017
def 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." )
975
1022
abs_path = abspath (path ) if path else None
976
1023
abs_entrypoint = abspath (entrypoint ) if entrypoint else None
977
1024
if not path and not entrypoint :
@@ -1228,7 +1275,7 @@ def validate_file_is_notebook(file_name):
1228
1275
raise RSConnectException ("A Jupyter notebook (.ipynb) file is required here." )
1229
1276
1230
1277
1231
- def validate_extra_files (directory , extra_files ):
1278
+ def validate_extra_files (directory , extra_files , use_abspath = False ):
1232
1279
"""
1233
1280
If the user specified a list of extra files, validate that they all exist and are
1234
1281
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):
1248
1295
raise RSConnectException ("%s must be under %s." % (extra_file , directory ))
1249
1296
if not exists (join (directory , extra_file )):
1250
1297
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
1251
1299
result .append (extra_file )
1252
1300
return result
1253
1301
@@ -1646,9 +1694,9 @@ def create_voila_manifest(
1646
1694
1647
1695
manifest .add_to_buffer (join (deploy_dir , environment .filename ), environment .contents )
1648
1696
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 )
1652
1700
return manifest
1653
1701
1654
1702
0 commit comments