1
+ from __future__ import annotations
1
2
import copy
2
3
import os
3
4
import re
@@ -247,7 +248,8 @@ def create_skeleton_instance(
247
248
"useSequenceForReview" : data .get ("useSequenceForReview" , True ),
248
249
# map inputVersions `ObjectId` -> `str` so json supports it
249
250
"inputVersions" : list (map (str , data .get ("inputVersions" , []))),
250
- "colorspace" : data .get ("colorspace" )
251
+ "colorspace" : data .get ("colorspace" ),
252
+ "hasExplicitFrames" : data .get ("hasExplicitFrames" )
251
253
}
252
254
253
255
if data .get ("renderlayer" ):
@@ -324,8 +326,8 @@ def prepare_representations(
324
326
skip_integration_repre_list (list): exclude specific extensions,
325
327
do_not_add_review (bool): explicitly skip review
326
328
color_managed_plugin (publish.ColormanagedPyblishPluginMixin)
327
- frames_to_render (str): implicit or explicit range of frames to render
328
- this value is sent to Deadline in JobInfo.Frames
329
+ frames_to_render (str | None ): implicit or explicit range of frames
330
+ to render this value is sent to Deadline in JobInfo.Frames
329
331
Returns:
330
332
list of representations
331
333
@@ -337,7 +339,7 @@ def prepare_representations(
337
339
log = Logger .get_logger ("farm_publishing" )
338
340
339
341
if frames_to_render is not None :
340
- frames_to_render = _get_real_frames_to_render (frames_to_render )
342
+ frames_to_render = convert_frames_str_to_list (frames_to_render )
341
343
else :
342
344
# Backwards compatibility for older logic
343
345
frame_start = int (skeleton_data .get ("frameStartHandle" ))
@@ -386,17 +388,21 @@ def prepare_representations(
386
388
frame_start -= 1
387
389
frames_to_render .insert (0 , frame_start )
388
390
389
- files = _get_real_files_to_render (collection , frames_to_render )
391
+ filenames = [
392
+ os .path .basename (filepath )
393
+ for filepath in _get_real_files_to_render (
394
+ collection , frames_to_render
395
+ )
396
+ ]
390
397
# explicitly disable review by user
391
398
preview = preview and not do_not_add_review
392
399
rep = {
393
400
"name" : ext ,
394
401
"ext" : ext ,
395
- "files" : files ,
402
+ "files" : filenames ,
403
+ "stagingDir" : staging ,
396
404
"frameStart" : frame_start ,
397
405
"frameEnd" : frame_end ,
398
- # If expectedFile are absolute, we need only filenames
399
- "stagingDir" : staging ,
400
406
"fps" : skeleton_data .get ("fps" ),
401
407
"tags" : ["review" ] if preview else [],
402
408
}
@@ -475,21 +481,45 @@ def prepare_representations(
475
481
return representations
476
482
477
483
478
- def _get_real_frames_to_render (frames ):
479
- """Returns list of frames that should be rendered.
484
+ def convert_frames_str_to_list (frames : str ) -> list [int ]:
485
+ """Convert frames definition string to frames.
486
+
487
+ Handles formats as:
488
+ >>> convert_frames_str_to_list('1001')
489
+ [1001]
490
+ >>> convert_frames_str_to_list('1002,1004')
491
+ [1002, 1004]
492
+ >>> convert_frames_str_to_list('1003-1005')
493
+ [1003, 1004, 1005]
494
+ >>> convert_frames_str_to_list('1001-1021x5')
495
+ [1001, 1006, 1011, 1016, 1021]
496
+
497
+ Args:
498
+ frames (str): String with frames definition.
499
+
500
+ Returns:
501
+ list[int]: List of frames.
480
502
481
- Artists could want to selectively render only particular frames
482
503
"""
483
- frames_to_render = []
504
+ step_pattern = re .compile (r"(?:step|by|every|x|:)(\d+)$" )
505
+
506
+ output = []
507
+ step = 1
484
508
for frame in frames .split ("," ):
485
509
if "-" in frame :
486
- splitted = frame .split ("-" )
487
- frames_to_render .extend (
488
- range (int (splitted [0 ]), int (splitted [1 ])+ 1 ))
510
+ frame_start , frame_end = frame .split ("-" )
511
+ match = step_pattern .findall (frame_end )
512
+ if match :
513
+ step = int (match [0 ])
514
+ frame_end = re .sub (step_pattern , "" , frame_end )
515
+
516
+ output .extend (
517
+ range (int (frame_start ), int (frame_end ) + 1 , step )
518
+ )
489
519
else :
490
- frames_to_render .append (int (frame ))
491
- frames_to_render .sort ()
492
- return frames_to_render
520
+ output .append (int (frame ))
521
+ output .sort ()
522
+ return output
493
523
494
524
495
525
def _get_real_files_to_render (collection , frames_to_render ):
@@ -502,22 +532,23 @@ def _get_real_files_to_render(collection, frames_to_render):
502
532
This range would override and filter previously prepared expected files
503
533
from DCC.
504
534
535
+ Example:
536
+ >>> expected_files = clique.parse([
537
+ >>> "foo_v01.0001.exr",
538
+ >>> "foo_v01.0002.exr",
539
+ >>> ])
540
+ >>> frames_to_render = [1]
541
+ >>> _get_real_files_to_render(expected_files, frames_to_render)
542
+ ["foo_v01.0001.exr"]
543
+
505
544
Args:
506
545
collection (clique.Collection): absolute paths
507
546
frames_to_render (list[int]): of int 1001
547
+
508
548
Returns:
509
- (list[str])
549
+ list[str]: absolute paths of files to be rendered
550
+
510
551
511
- Example:
512
- --------
513
-
514
- expectedFiles = [
515
- "foo_v01.0001.exr",
516
- "foo_v01.0002.exr",
517
- ]
518
- frames_to_render = 1
519
- >>
520
- ["foo_v01.0001.exr"] - only explicitly requested frame returned
521
552
"""
522
553
included_frames = set (collection .indexes ).intersection (frames_to_render )
523
554
real_collection = clique .Collection (
@@ -526,13 +557,17 @@ def _get_real_files_to_render(collection, frames_to_render):
526
557
collection .padding ,
527
558
indexes = included_frames
528
559
)
529
- real_full_paths = list (real_collection )
530
- return [os .path .basename (file_url ) for file_url in real_full_paths ]
560
+ return list (real_collection )
531
561
532
562
533
- def create_instances_for_aov (instance , skeleton , aov_filter ,
534
- skip_integration_repre_list ,
535
- do_not_add_review ):
563
+ def create_instances_for_aov (
564
+ instance ,
565
+ skeleton ,
566
+ aov_filter ,
567
+ skip_integration_repre_list ,
568
+ do_not_add_review ,
569
+ frames_to_render = None
570
+ ):
536
571
"""Create instances from AOVs.
537
572
538
573
This will create new pyblish.api.Instances by going over expected
@@ -544,6 +579,7 @@ def create_instances_for_aov(instance, skeleton, aov_filter,
544
579
aov_filter (dict): AOV filter.
545
580
skip_integration_repre_list (list): skip
546
581
do_not_add_review (bool): Explicitly disable reviews
582
+ frames_to_render (str | None): Frames to render.
547
583
548
584
Returns:
549
585
list of pyblish.api.Instance: Instances created from
@@ -590,7 +626,8 @@ def create_instances_for_aov(instance, skeleton, aov_filter,
590
626
aov_filter ,
591
627
additional_color_data ,
592
628
skip_integration_repre_list ,
593
- do_not_add_review
629
+ do_not_add_review ,
630
+ frames_to_render
594
631
)
595
632
596
633
@@ -719,8 +756,15 @@ def get_product_name_and_group_from_template(
719
756
return resulting_product_name , resulting_group_name
720
757
721
758
722
- def _create_instances_for_aov (instance , skeleton , aov_filter , additional_data ,
723
- skip_integration_repre_list , do_not_add_review ):
759
+ def _create_instances_for_aov (
760
+ instance ,
761
+ skeleton ,
762
+ aov_filter ,
763
+ additional_data ,
764
+ skip_integration_repre_list ,
765
+ do_not_add_review ,
766
+ frames_to_render
767
+ ):
724
768
"""Create instance for each AOV found.
725
769
726
770
This will create new instance for every AOV it can detect in expected
@@ -734,7 +778,8 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
734
778
skip_integration_repre_list (list): list of extensions that shouldn't
735
779
be published
736
780
do_not_add_review (bool): explicitly disable review
737
-
781
+ frames_to_render (str | None): implicit or explicit range of
782
+ frames to render this value is sent to Deadline in JobInfo.Frames
738
783
739
784
Returns:
740
785
list of instances
@@ -754,10 +799,23 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
754
799
# go through AOVs in expected files
755
800
for aov , files in expected_files [0 ].items ():
756
801
collected_files = _collect_expected_files_for_aov (files )
757
-
758
- expected_filepath = collected_files
759
- if isinstance (collected_files , (list , tuple )):
760
- expected_filepath = collected_files [0 ]
802
+ first_filepath = collected_files
803
+ if isinstance (first_filepath , (list , tuple )):
804
+ first_filepath = first_filepath [0 ]
805
+ staging_dir = os .path .dirname (first_filepath )
806
+
807
+ if (
808
+ frames_to_render is not None
809
+ and isinstance (collected_files , (list , tuple )) # not single file
810
+ ):
811
+ aov_frames_to_render = convert_frames_str_to_list (frames_to_render )
812
+ collections , _ = clique .assemble (collected_files )
813
+ collected_files = _get_real_files_to_render (
814
+ collections [0 ], aov_frames_to_render )
815
+ else :
816
+ frame_start = int (skeleton .get ("frameStartHandle" ))
817
+ frame_end = int (skeleton .get ("frameEndHandle" ))
818
+ aov_frames_to_render = list (range (frame_start , frame_end + 1 ))
761
819
762
820
dynamic_data = {
763
821
"aov" : aov ,
@@ -768,7 +826,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
768
826
# TODO: this must be changed to be more robust. Any coincidence
769
827
# of camera name in the file path will be considered as
770
828
# camera name. This is not correct.
771
- camera = [cam for cam in cameras if cam in expected_filepath ]
829
+ camera = [cam for cam in cameras if cam in first_filepath ]
772
830
773
831
# Is there just one camera matching?
774
832
# TODO: this is not true, we can have multiple cameras in the scene
@@ -813,18 +871,16 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
813
871
dynamic_data = dynamic_data
814
872
)
815
873
816
- staging = os .path .dirname (expected_filepath )
817
-
818
874
try :
819
- staging = remap_source (staging , anatomy )
875
+ staging_dir = remap_source (staging_dir , anatomy )
820
876
except ValueError as e :
821
877
log .warning (e )
822
878
823
879
log .info ("Creating data for: {}" .format (product_name ))
824
880
825
881
app = os .environ .get ("AYON_HOST_NAME" , "" )
826
882
827
- render_file_name = os .path .basename (expected_filepath )
883
+ render_file_name = os .path .basename (first_filepath )
828
884
829
885
aov_patterns = aov_filter
830
886
@@ -881,10 +937,10 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
881
937
"name" : ext ,
882
938
"ext" : ext ,
883
939
"files" : collected_files ,
884
- "frameStart" : int ( skeleton [ "frameStartHandle" ]) ,
885
- "frameEnd" : int ( skeleton [ "frameEndHandle" ]) ,
940
+ "frameStart" : aov_frames_to_render [ 0 ] ,
941
+ "frameEnd" : aov_frames_to_render [ - 1 ] ,
886
942
# If expectedFile are absolute, we need only filenames
887
- "stagingDir" : staging ,
943
+ "stagingDir" : staging_dir ,
888
944
"fps" : new_instance .get ("fps" ),
889
945
"tags" : ["review" ] if preview else [],
890
946
"colorspaceData" : {
0 commit comments