forked from shotgunsoftware/tk-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontext.py
More file actions
1812 lines (1482 loc) · 77.6 KB
/
context.py
File metadata and controls
1812 lines (1482 loc) · 77.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright (c) 2013 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
"""
Management of the current context, e.g. the current shotgun entity/step/task.
"""
import os
import pickle
import copy
from tank_vendor import yaml
from . import authentication
from .util import login
from .util import shotgun_entity
from .util import shotgun
from . import constants
from .errors import TankError, TankContextDeserializationError
from .path_cache import PathCache
from .template import TemplatePath
class Context(object):
"""
A context instance is used to collect a set of key fields describing the
current Context. We sometimes refer to the context as the current work area.
Typically this would be the current shot or asset that someone is working on.
The context captures the current point in both shotgun and the file system and context
objects are launch a toolkit engine via the :meth:`sgtk.platform.start_engine`
method. The context points the engine to a particular
point in shotgun and on disk - it could be something as detailed as a task inside a Shot,
and something as vague as an empty context.
The context is split up into several levels of granularity, reflecting the
fundamental hierarchy of Shotgun itself.
- The project level defines which shotgun project the context reflects.
- The entity level defines which entity the context reflects. For example,
this may be a Shot or an Asset. Note that in the case of a Shot, the context
does not contain any direct information of which sequence the shot is linked to,
however the context can still resolve such relationships implicitly if needed -
typically via the :meth:`Context.as_context_fields` method.
- The step level defines the current pipeline step. This is often a reflection of a
department or a general step in a workflow or pipeline (e.g. Modeling, Rigging).
- The task level defines a current Shotgun task.
- The user level defines the current user.
The data forms a hierarchy, so implicitly, the task belongs to the entity which in turn
belongs to the project. The exception to this is the user, which simply reflects the
currently operating user.
"""
def __init__(
self, tk, project=None, entity=None, step=None, task=None, user=None,
additional_entities=None, source_entity=None
):
"""
Context objects are not constructed by hand but are fabricated by the
methods :meth:`Sgtk.context_from_entity`, :meth:`Sgtk.context_from_entity_dictionary`
and :meth:`Sgtk.context_from_path`.
"""
self.__tk = tk
self.__project = project
self.__entity = entity
self.__step = step
self.__task = task
self.__user = user
self.__additional_entities = additional_entities or []
self.__source_entity = source_entity
self._entity_fields_cache = {}
def __repr__(self):
# multi line repr
msg = []
msg.append(" Project: %s" % str(self.__project))
msg.append(" Entity: %s" % str(self.__entity))
msg.append(" Step: %s" % str(self.__step))
msg.append(" Task: %s" % str(self.__task))
msg.append(" User: %s" % str(self.__user))
msg.append(" Shotgun URL: %s" % self.shotgun_url)
msg.append(" Additional Entities: %s" % str(self.__additional_entities))
msg.append(" Source Entity: %s" % str(self.__source_entity))
return "<Sgtk Context: %s>" % ("\n".join(msg))
def __str__(self):
"""
String representation for context
"""
if self.project is None:
# We're in a "site" context, so we'll give the site's url
# minus the "https://" if that's attached.
ctx_name = self.shotgun_url.split("//")[-1]
elif self.entity is None:
# project-only, e.g 'Project foobar'
ctx_name = "Project %s" % self.project.get("name")
elif self.step is None and self.task is None:
# entity only
# e.g. Shot ABC_123
# resolve custom entities to their real display
entity_display_name = shotgun.get_entity_type_display_name(
self.__tk,
self.entity.get("type")
)
ctx_name = "%s %s" % (
entity_display_name,
self.entity.get("name")
)
else:
# we have either step or task
task_step = None
if self.step:
task_step = self.step.get("name")
if self.task:
task_step = self.task.get("name")
# e.g. Lighting, Shot ABC_123
# resolve custom entities to their real display
entity_display_name = shotgun.get_entity_type_display_name(
self.__tk,
self.entity.get("type")
)
ctx_name = "%s, %s %s" % (
task_step,
entity_display_name,
self.entity.get("name")
)
return ctx_name
def __eq__(self, other):
"""
Test if this Context instance is equal to the other Context instance
:param other: The other Context instance to compare with
:returns: True if self represents the same context as other,
otherwise False
"""
def _entity_dicts_eq(d1, d2):
"""
Test to see if two entity dictionaries are equal. They are considered
equal if both are dictionaries containing 'type' and 'id' with the same
values for both keys, For example:
Comparing these two dictionaries would return True:
- {"type":"Shot", "id":123, "foo":"foo"}
- {"type":"Shot", "id":123, "foo":"bar", "bar":"foo"}
But comparing these two dictionaries would return False:
- {"type":"Shot", "id":123, "foo":"foo"}
- {"type":"Shot", "id":567, "foo":"foo"}
:param d1: First entity dictionary
:param d2: Second entity dictionary
:returns: True if d1 and d2 are considered equal, otherwise False.
"""
if d1 == d2 == None:
return True
if d1 == None or d2 == None:
return False
return d1["type"] == d2["type"] and d1["id"] == d2["id"]
if not isinstance(other, Context):
return NotImplemented
if not _entity_dicts_eq(self.project, other.project):
return False
if not _entity_dicts_eq(self.entity, other.entity):
return False
if not _entity_dicts_eq(self.step, other.step):
return False
if not _entity_dicts_eq(self.task, other.task):
return False
# compare additional entities
if self.additional_entities and other.additional_entities:
# compare type, id tuples of all additional entities to ensure they are exactly the same.
# this compare ignores duplicates in either list and just ensures that the intersection
# of both lists contains all unique elements from both lists.
types_and_ids = set([(e["type"], e["id"]) for e in self.additional_entities if e])
other_types_and_ids = set([(e["type"], e["id"]) for e in other.additional_entities if e])
if types_and_ids != other_types_and_ids:
return False
elif self.additional_entities or other.additional_entities:
return False
# finally compare the user - this may result in a Shotgun look-up
# so do this last!
if not _entity_dicts_eq(self.user, other.user):
return False
return True
def __ne__(self, other):
"""
Test if this Context instance is not equal to the other Context instance
:param other: The other Context instance to compare with
:returns: True if self != other, False otherwise
"""
is_equal = self.__eq__(other)
if is_equal is NotImplemented:
return NotImplemented
return not is_equal
def __deepcopy__(self, memo):
"""
Allow Context objects to be deepcopied - Note that the tk
member is _never_ copied
"""
# construct copy with current api instance:
ctx_copy = Context(self.__tk)
# deepcopy all other members:
ctx_copy.__project = copy.deepcopy(self.__project, memo)
ctx_copy.__entity = copy.deepcopy(self.__entity, memo)
ctx_copy.__step = copy.deepcopy(self.__step, memo)
ctx_copy.__task = copy.deepcopy(self.__task, memo)
ctx_copy.__user = copy.deepcopy(self.__user, memo)
ctx_copy.__additional_entities = copy.deepcopy(self.__additional_entities, memo)
ctx_copy.__source_entity = copy.deepcopy(self.__source_entity, memo)
# except:
# ctx_copy._entity_fields_cache
return ctx_copy
################################################################################################
# properties
@property
def project(self):
"""
The shotgun project associated with this context.
If the context is incomplete, it is possible that the property is None. Example::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Light/work")
>>> ctx.project
{'type': 'Project', 'id': 4, 'name': 'demo_project'}
:returns: A std shotgun link dictionary with keys id, type and name, or None if not defined
"""
return self.__project
@property
def entity(self):
"""
The shotgun entity associated with this context.
If the context is incomplete, it is possible that the property is None. Example::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Light/work")
>>> ctx.entity
{'type': 'Shot', 'id': 412, 'name': 'ABC'}
:returns: A std shotgun link dictionary with keys id, type and name, or None if not defined
"""
return self.__entity
@property
def source_entity(self):
"""
The Shotgun entity that was used to construct this Context.
This is not necessarily the same as the context's "entity", as there
are situations where a context is interpreted from an input entity,
such as when a PublishedFile entity is used to determine a context. In
that case, the original PublishedFile becomes the source_entity, and
project, entity, task, and step are determined by what the
PublishedFile entity is linked to. A specific example of where this is
useful is in a pick_environment core hook. In that hook, an environment
is determined based on a provided Context object. In the case where we want
to provide a specific environment for a Context built from a PublishedFile
entity, the context's source_entity can be used to know for certain that it
was constructured from a PublishedFile.
:returns: A Shotgun entity dictionary.
:rtype: dict or None
"""
return self.__source_entity
@property
def step(self):
"""
The shotgun step associated with this context.
If the context is incomplete, it is possible that the property is None. Example::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Light/work")
>>> ctx.step
{'type': 'Step', 'id': 12, 'name': 'Light'}
:returns: A std shotgun link dictionary with keys id, type and name, or None if not defined
"""
return self.__step
@property
def task(self):
"""
The shotgun task associated with this context.
If the context is incomplete, it is possible that the property is None. Example::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Lighting/first_pass_lgt/work")
>>> ctx.task
{'type': 'Task', 'id': 212, 'name': 'first_pass_lgt'}
:returns: A std shotgun link dictionary with keys id, type and name, or None if not defined
"""
return self.__task
@property
def user(self):
"""
A property which holds the user associated with this context.
If the context is incomplete, it is possible that the property is None.
The user property is special - either it represents a user value that was baked
into a template path upon folder creation, or it represents the current user::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Lighting/dirk.gently/work")
>>> ctx.user
{'type': 'HumanUser', 'id': 23, 'name': 'Dirk Gently'}
:returns: A std shotgun link dictionary with keys id, type and name, or None if not defined
"""
# NOTE! get_shotgun_user returns more fields than just type, id and name
# so make sure we get rid of those. We should make sure we return the data
# in a consistent way, similar to all other entities. No more. No less.
if self.__user is None:
user = login.get_current_user(self.__tk)
if user is not None:
self.__user = {"type": user.get("type"),
"id": user.get("id"),
"name": user.get("name")}
return self.__user
@property
def additional_entities(self):
"""
List of entities that are required to provide a full context in non-standard configurations.
The "context_additional_entities" core hook gives the context construction code hints about how
this data should be populated.
.. warning:: This is an old and advanced option and may be deprecated in the future. We strongly
recommend not using it.
:returns: A list of std shotgun link dictionaries.
Will be an empty list in most cases.
"""
return self.__additional_entities
@property
def entity_locations(self):
"""
A list of paths on disk which correspond to the **entity** which this context represents.
If no folders have been created for this context yet, the value of this property will be an empty list::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_entity("Task", 8)
>>> ctx.entity_locations
['/studio.08/demo_project/sequences/AAA/ABC']
:returns: A list of paths
"""
if self.entity is None:
return []
paths = self.__tk.paths_from_entity(self.entity["type"], self.entity["id"])
return paths
@property
def shotgun_url(self):
"""
Returns the shotgun detail page url that best represents this context. Depending on
the context, this may be a task, a shot, an asset or a project. If the context is
completely empty, the root url of the associated shotgun installation is returned.
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_entity("Task", 8)
>>> ctx.shotgun_url
'https://mystudio.shotgunstudio.com/detail/Task/8'
"""
# walk up task -> entity -> project -> site
if self.task is not None:
return "%s/detail/%s/%d" % (self.__tk.shotgun_url, "Task", self.task["id"])
if self.entity is not None:
return "%s/detail/%s/%d" % (self.__tk.shotgun_url, self.entity["type"], self.entity["id"])
if self.project is not None:
return "%s/detail/%s/%d" % (self.__tk.shotgun_url, "Project", self.project["id"])
# fall back on just the site main url
return self.__tk.shotgun_url
@property
def filesystem_locations(self):
"""
A property which holds a list of paths on disk which correspond to this context.
If no folders have been created for this context yet, the value of this property will be an empty list::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_entity("Task", 8)
>>> ctx.filesystem_locations
['/studio.08/demo_project/sequences/AAA/ABC/light/initial_pass']
:returns: A list of paths
"""
# first handle special cases: empty context
if self.project is None:
return []
# first handle special cases: project context
if self.entity is None:
return self.__tk.paths_from_entity("Project", self.project["id"])
# at this stage we know that the context contains an entity
# start off with all the paths matching this entity and then cull it down
# based on constraints.
entity_paths = self.__tk.paths_from_entity(self.entity["type"], self.entity["id"])
# for each of these paths, get the context and compare it against our context
# todo: optimize this!
matching_paths = []
for p in entity_paths:
ctx = self.__tk.context_from_path(p)
# the stuff we need to compare against are all the "child" levels
# below entity: task and user
matching = False
if ctx.user is None and self.user is None:
# no user data in either context
matching = True
elif ctx.user is not None and self.user is not None:
# both contexts have user data - is it matching?
if ctx.user["id"] == self.user["id"]:
matching = True
if matching:
# ok so user looks good, now check task.
# it is possible that with a context that comes from shotgun
# there is a task populated which is not being used in the file system
# so when we compare tasks, only if there are differing task ids,
# we should treat it as a mismatch.
task_matching = True
if ctx.task is not None and self.task is not None:
if ctx.task["id"] != self.task["id"]:
task_matching = False
if task_matching:
# both user and task is matching
matching_paths.append(p)
return matching_paths
@property
def sgtk(self):
"""
The Toolkit API instance associated with this context
:returns: :class:`Sgtk`
"""
return self.__tk
@property
def tank(self):
"""
Legacy equivalent of :meth:`sgtk`
:returns: :class:`Sgtk`
"""
return self.__tk
################################################################################################
# public methods
def as_template_fields(self, template, validate=False):
"""
Returns the context object as a dictionary of template fields.
This is useful if you want to use a Context object as part of a call to
the Sgtk API. In order for the system to pass suitable values, you need to
pass the template you intend to use the data with as a parameter to this method.
The values are derived from existing paths on disk, or in the case of keys with
shotgun_entity_type and shotgun_entity_field settings, direct queries to the Shotgun
server. The validate parameter can be used to ensure that the method returns all
context fields required by the template and if it can't then a :class:`TankError` will be raised.
Example::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
# Create a template based on a path on disk. Because this path has been
# generated through Toolkit's folder processing and there are corresponding
# FilesystemLocation entities stored in Shotgun, the context can resolve
# the path into a set of Shotgun entities.
#
# Note how the context object, once resolved, does not contain
# any information about the sequence associated with the Shot.
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Lighting/work")
>>> ctx.project
{'type': 'Project', 'id': 4, 'name': 'demo_project'}
>>> ctx.entity
{'type': 'Shot', 'id': 2, 'name': 'ABC'}
>>> ctx.step
{'type': 'Step', 'id': 1, 'name': 'Light'}
# now if we have a template object that we want to turn into a path,
# we can request that the context object attempts to resolve as many
# fields as it can. These fields can then be plugged into the template
# object to generate a path on disk
>>> templ = tk.templates["maya_shot_publish"]
>>> templ
<Sgtk TemplatePath maya_shot_publish: sequences/{Sequence}/{Shot}/{Step}/publish/{name}.v{version}.ma>
>>> fields = ctx.as_template_fields(templ)
>>> fields
{'Step': 'Lighting', 'Shot': 'ABC', 'Sequence': 'AAA'}
# the fields dictionary above contains all the 'high level' data that is necessary to realise
# the template path. An app or integration can now go ahead and populate the fields specific
# for the app's business logic - in this case name and version - and resolve the fields dictionary
# data into a path.
:param template: :class:`Template` for which the fields will be used.
:param validate: If True then the fields found will be checked to ensure that all expected fields for
the context were found. If a field is missing then a :class:`TankError` will be raised
:returns: A dictionary of template files representing the context. Handy to pass to for example
:meth:`Template.apply_fields`.
:raises: :class:`TankError` if the fields can't be resolved for some reason or if 'validate' is True
and any of the context fields for the template weren't found.
"""
# Get all entities into a dictionary
entities = {}
if self.entity:
entities[self.entity["type"]] = self.entity
if self.step:
entities["Step"] = self.step
if self.task:
entities["Task"] = self.task
if self.user:
entities["HumanUser"] = self.user
if self.project:
entities["Project"] = self.project
# If there are any additional entities, use them as long as they don't
# conflict with types we already have values for (Step, Task, Shot/Asset/etc)
for add_entity in self.additional_entities:
if add_entity["type"] not in entities:
entities[add_entity["type"]] = add_entity
fields = {}
# Try to populate fields using paths caches for entity
if isinstance(template, TemplatePath):
# first, sanity check that we actually have a path cache entry
# this relates to ticket 22541 where it is possible to create
# a context object purely from Shotgun without having it in the path cache
# (using tk.context_from_entity(Task, 1234) for example)
#
# Such a context can result in erronous lookups in the later commands
# since these make the assumption that the path cache contains the information
# that is being saught after.
#
# therefore, if the context object contains an entity object and this entity is
# not represented in the path cache, raise an exception.
if self.entity and len(self.entity_locations) == 0:
# context has an entity associated but no path cache entries
raise TankError("Cannot resolve template data for context '%s' - this context "
"does not have any associated folders created on disk yet and "
"therefore no template data can be extracted. Please run the folder "
"creation for %s and try again!" % (self, self.shotgun_url))
# first look at which ENTITY paths are associated with this context object
# and use these to extract the right fields for this template
fields = self._fields_from_entity_paths(template)
# filter the list of fields to just those that don't have a 'None' value.
# Note: A 'None' value for a field indicates an ambiguity and was set in the
# _fields_from_entity_paths method (!)
non_none_fields = dict([(key, value) for key, value in fields.iteritems() if value is not None])
# Determine additional field values by walking down the template tree
fields.update(self._fields_from_template_tree(template, non_none_fields, entities))
# get values for shotgun query keys in template
fields.update(self._fields_from_shotgun(template, entities, validate))
if validate:
# check that all context template fields were found and if not then raise a TankError
missing_fields = []
for key_name in template.keys.keys():
if key_name in entities and key_name not in fields:
# we have a template key that should have been found but wasn't!
missing_fields.append(key_name)
if missing_fields:
raise TankError("Cannot resolve template fields for context '%s' - the following "
"keys could not be resolved: '%s'. Please run the folder creation "
"for '%s' and try again!"
% (self, ", ".join(missing_fields), self.shotgun_url))
return fields
def create_copy_for_user(self, user):
"""
Provides the ability to create a copy of an existing Context for a specific user.
This is useful if you need to determine a user specific version of a path, e.g.
when copying files between different user sandboxes. Example::
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Lighting/dirk.gently/work")
>>> ctx.user
{'type': 'HumanUser', 'id': 23, 'name': 'Dirk Gently'}
>>>
>>> copied_ctx = tk.create_copy_for_user({'type': 'HumanUser', 'id': 7, 'name': 'John Snow'})
>>> copied_ctx.user
{'type': 'HumanUser', 'id': 23, 'name': 'John Snow'}
:param user: The Shotgun user entity dictionary that should be set on the copied context
:returns: :class:`Context`
"""
ctx_copy = copy.deepcopy(self)
ctx_copy.__user = user
return ctx_copy
################################################################################################
# serialization
def serialize(self, with_user_credentials=True):
"""
Serializes the context into a string.
Any Context object can be serialized to/deserialized from a string.
This can be useful if you need to pass a Context between different processes.
As an example, the ``tk-multi-launchapp`` uses this mechanism to pass the Context
from the launch process (e.g. for example Shotgun Desktop) to the
Application (e.g. Maya) being launched. Example:
>>> import sgtk
>>> tk = sgtk.sgtk_from_path("/studio.08/demo_project")
>>> ctx = tk.context_from_path("/studio.08/demo_project/sequences/AAA/ABC/Lighting/dirk.gently/work")
>>> context_str = ctx.serialize(ctx)
>>> new_ctx = sgtk.Context.deserialize(context_str)
:param with_user_credentials: If ``True``, the currently authenticated user's credentials, as
returned by :meth:`sgtk.get_authenticated_user`, will also be serialized with the context.
.. note:: For example, credentials should be omitted (``with_user_credentials=False``) when
serializing the context from a user's current session to send it to a render farm. By doing
so, invoking :meth:`sgtk.Context.deserialize` on the render farm will only restore the
context and not the authenticated user.
:returns: String representation
"""
# Avoids cyclic imports
from .api import get_authenticated_user
data = {
"project": self.project,
"entity": self.entity,
"user": self.user,
"step": self.step,
"task": self.task,
"additional_entities": self.additional_entities,
"source_entity": self.source_entity,
"_pc_path": self.tank.pipeline_configuration.get_path()
}
if with_user_credentials:
# If there is an authenticated user.
user = get_authenticated_user()
if user:
# We should serialize it as well so that the next process knows who to
# run as.
data["_current_user"] = authentication.serialize_user(user)
return pickle.dumps(data)
@classmethod
def deserialize(cls, context_str):
"""
The inverse of :meth:`Context.serialize`.
:param context_str: String representation of context, created with :meth:`Context.serialize`
.. note:: If the context was serialized with the user credentials, the currently authenticated
user will be updated with these credentials.
:returns: :class:`Context`
"""
# lazy load this to avoid cyclic dependencies
from .api import Tank, set_authenticated_user
try:
data = pickle.loads(context_str)
except Exception as e:
raise TankContextDeserializationError(str(e))
# first get the pipeline config path out of the dict
pipeline_config_path = data["_pc_path"]
del data["_pc_path"]
# Authentication in Toolkit requires that credentials are passed from
# one process to another so the currently authenticated user is carried
# from one process to another. The current user needs to be part of the
# context because multiple DCCs can run at the same time under different
# users, e.g. launching Maya from the site as user A and Nuke from the tank
# command as user B.
user_string = data.get("_current_user")
if user_string:
# Remove it from the data
del data["_current_user"]
# and set the authenticated user user.
user = authentication.deserialize_user(user_string)
set_authenticated_user(user)
# create a Sgtk API instance.
tk = Tank(pipeline_config_path)
# add it to the constructor instance
data["tk"] = tk
# and lastly make the obejct
return cls(**data)
################################################################################################
# private methods
def _fields_from_shotgun(self, template, entities, validate):
"""
Query Shotgun server for keys used by this template whose values come directly
from Shotgun fields.
:param template: Template to retrieve Shotgun fields for.
:param entities: Dictionary of entities for the current context.
:param validate: If True, missing fields will raise a TankError.
:returns: Dictionary of field values extracted from Shotgun.
:rtype: dict
:raises TankError: Raised if a key is missing from the entities list when ``validate`` is ``True``.
"""
fields = {}
# for any sg query field
for key in template.keys.values():
# check each key to see if it has shotgun query information that we should resolve
if key.shotgun_field_name:
# this key is a shotgun value that needs fetching!
# ensure that the context actually provides the desired entities
if not key.shotgun_entity_type in entities:
if validate:
raise TankError("Key '%s' in template '%s' could not be populated by "
"context '%s' because the context does not contain a "
"shotgun entity of type '%s'!" % (key, template, self, key.shotgun_entity_type))
else:
continue
entity = entities[key.shotgun_entity_type]
# check the context cache
cache_key = (entity["type"], entity["id"], key.shotgun_field_name)
if cache_key in self._entity_fields_cache:
# already have the value cached - no need to fetch from shotgun
fields[key.name] = self._entity_fields_cache[cache_key]
else:
# get the value from shotgun
filters = [["id", "is", entity["id"]]]
query_fields = [key.shotgun_field_name]
result = self.__tk.shotgun.find_one(key.shotgun_entity_type, filters, query_fields)
if not result:
# no record with that id in shotgun!
raise TankError("Could not retrieve Shotgun data for key '%s' in "
"template '%s'. No records in Shotgun are matching "
"entity '%s' (Which is part of the current "
"context '%s')" % (key, template, entity, self))
value = result.get(key.shotgun_field_name)
# note! It is perfectly possible (and may be valid) to return None values from
# shotgun at this point. In these cases, a None field will be returned in the
# fields dictionary from as_template_fields, and this may be injected into
# a template with optional fields.
if value is None:
processed_val = None
else:
# now convert the shotgun value to a string.
# note! This means that there is no way currently to create an int key
# in a tank template which matches an int field in shotgun, since we are
# force converting everything into strings...
processed_val = shotgun_entity.sg_entity_to_string(self.__tk,
key.shotgun_entity_type,
entity.get("id"),
key.shotgun_field_name,
value)
if not key.validate(processed_val):
raise TankError("Template validation failed for value '%s'. This "
"value was retrieved from entity %s in Shotgun to "
"represent key '%s' in "
"template '%s'." % (processed_val, entity, key, template))
# all good!
# populate dictionary and cache
fields[key.name] = processed_val
self._entity_fields_cache[cache_key] = processed_val
return fields
def _fields_from_entity_paths(self, template):
"""
Determines a template's key values based on context by walking up the context entities paths until
matches for the template are found.
:param template: The template to find fields for
:returns: A dictionary of field name, value pairs for any fields found for the template
"""
fields = {}
project_roots = self.__tk.pipeline_configuration.get_data_roots().values()
# get all locations on disk for our context object from the path cache
path_cache_locations = self.entity_locations
# now loop over all those locations and check if one of the locations
# are matching the template that is passed in. In that case, try to
# extract the fields values.
for cur_path in path_cache_locations:
# walk up path until we reach the project root and get values
while cur_path not in project_roots:
cur_fields = template.validate_and_get_fields(cur_path)
if cur_fields is not None:
# If there are conflicts, there is ambiguity in the schema
for key, value in cur_fields.items():
if value != fields.get(key, value):
# Value is ambiguous for this key
cur_fields[key] = None
fields.update(cur_fields)
break
else:
cur_path = os.path.dirname(cur_path)
return fields
def _fields_from_template_tree(self, template, known_fields, context_entities):
"""
Determines values for a template's keys based on the context by walking down the template tree
matching template keys with entity types.
This method attempts to find as many fields as possible from the path cache but will try to ensure
that incorrect fields are never returned, even if the path cache is not 100% clean (e.g. contains
out-of-date paths for one or more of the entities in the context).
:param template: The template to find fields for
:param known_fields: Dictionary of fields that are already known for this template. The
logic in this method will ensure that any fields found match these.
:param context_entities: A dictionary of {entity_type:entity_dict} that contains all the entities
belonging to this context.
:returns: A dictionary of all fields found by this method
"""
# Step 1 - Walk up the template tree and collect templates
#
# Use cached paths to find field values
# these will be returned in top-down order:
# [<Sgtk TemplatePath sequences/{Sequence}>,
# <Sgtk TemplatePath sequences/{Sequence}/{Shot}>,
# <Sgtk TemplatePath sequences/{Sequence}/{Shot}/{Step}>,
# <Sgtk TemplatePath sequences/{Sequence}/{Shot}/{Step}/publish>,
# <Sgtk TemplatePath sequences/{Sequence}/{Shot}/{Step}/publish/maya>,
# <Sgtk TemplatePath maya_shot_publish: sequences/{Sequence}/{Shot}/{Step}/publish/maya/{name}.v{version}.ma>]
templates = _get_template_ancestors(template)
# Step 2 - walk templates from the root down.
# for each template, get all paths we have stored in the database and find any fields we can for it, making
# sure that none of the found fields conflict with the list of entities provided to this method
#
# build up a list of fields as we go so that each level matches
# at least the fields from all previous levels
found_fields = {}
# get a path cache handle
path_cache = PathCache(self.__tk)
try:
for template in templates:
# iterate over all keys in the {key_name:key} dictionary for the template
# looking for any that represent context entities (key name == entity type)
template_key_dict = template.keys
for key_name in template_key_dict.keys():
# Check to see if we already have a value for this key:
if key_name in known_fields or key_name in found_fields:
# already have a value so skip
continue
if key_name not in context_entities:
# key doesn't represent an entity so skip
continue
# find fields for any paths associated with this entity by looking in the path cache:
entity_fields = _values_from_path_cache(context_entities[key_name], template, path_cache,
required_fields=found_fields)
# entity_fields may contain additional fields that correspond to entities
# so we should be sure to validate these as well if we can.
#
# The following example illustrates where the code could previously return incorrect entity
# information from this method:
#
# With the following template:
# /{Sequence}/{Shot}/{Step}
#
# And a path cache that contains:
# Type | Id | Name | Path
# ----------------------------------------------------
# Sequence | 001 | Seq_001 | /Seq_001
# Shot | 002 | Shot_A | /Seq_001/Shot_A
# Step | 003 | Lighting | /Seq_001/Shot_A/Lighting
# Step | 003 | Lighting | /Seq_001/blah/Shot_B/Lighting <- this is out of date!
# Shot | 004 | Shot_B | /Seq_001/blah/Shot_B <- this is out of date!
#
# (Note: the schema/templates have been changed since the entries for Shot_b were added)
#
# The sub-templates used to search for fields are:
# /{Sequence}
# /{Sequence}/{Shot}
# /{Sequence}/{Shot}/{Step}
#
# And the entities passed into the method are:
# Sequence: Seq_001
# Shot: Shot_B
# Step: Lighting
#
# We are searching for fields for 'Shot_B' that has a broken entry in the path cache so the fields
# returned for each level of the template will be:
# /{Sequence} -> {"Sequence":"Seq_001"} <- Correct
# /{Sequence}/{Shot} -> {} <- entry not found for Shot_B matching
# the template
# /{Sequence}/{Shot}/{Step} -> {"Sequence":"Seq_001", <- Correct
# "Shot":"Shot_A", <- Wrong!
# "Step":"Lighting"} <- Correct
#
# In previous implementations, the final fields would incorrectly be returned as:
#
# {"Sequence":"Seq_001",
# "Shot":"Shot_A",
# "Step":"Lighting"}
#
# The wrong Shot (Shot_A) is returned and not caught because the code only tested that the Step