app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
Drew Owens
The mechanical layer on 1.12.8 with the restart fixes (1.13.0-dev.1, add-in 1.0.23)
2ef0220
15d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
"""Session-owned public controller for finite exploded presentation graphics.
Direct synchronous WRITES use the mechanical durable journal, not PTransaction.
History receipts describe past completion; this registry describes live resources.
No document create/close, global Undo, timer, capture or cross-session adoption.
"""
import copy
import hashlib
import json
from pathlib import Path
import time
import uuid
import adsk.fusion
from . import core
from . import exploded_preview_prototype as backend
from .core import MechanicalError, SESSION_ID, require, same
from .journal import fingerprint
from .exploded_view import request_arguments
SCHEMA_VERSION = 1
BACKEND_SOURCE_DIGEST = hashlib.sha256(Path(backend.__file__).read_bytes()).hexdigest()
MAX_ACTIVE = 4
MAX_ENTRIES = 128
MAX_PLAN_BYTES = 2 * 1024 * 1024
MAX_TOTAL_PLAN_BYTES = 8 * 1024 * 1024
MAX_TOTAL_BODIES = 512
def bounded_text(value, name, maximum=128):
require(isinstance(value, str) and 0 < len(value) <= maximum, 'invalid_input', name + ' must be a bounded nonempty string.')
return value
def exact_target(value):
require(isinstance(value, dict) and set(value) == {'documentId', 'revision'}, 'invalid_input', 'An exact documentId/revision target is required.')
for name in value:
bounded_text(value[name], name, 256)
return copy.deepcopy(value)
def flags(doc):
result = (doc.isSaved, doc.isModified)
require(all(type(v) is bool for v in result), 'preview_state_unavailable', 'Document flags are unavailable.')
return result
def known_identity(app, document_id):
"""Resolve only an already-issued session identity; no revision adoption."""
backend.main_thread()
matches = [doc for doc, key in core._documents if key == document_id]
require(len(matches) == 1 and matches[0].isValid, 'preview_document_unavailable', 'Exact session document is unavailable.')
doc = matches[0]
require(sum(same(item, doc) for item in backend.bounded(app.documents, 128)) == 1,
'preview_document_unavailable', 'Exact session document is not uniquely open.')
design = adsk.fusion.Design.cast(doc.products.itemByProductType('DesignProductType'))
require(design is not None and design.isValid and same(design.parentDocument, doc),
'preview_document_unavailable', 'Exact document has no matching Design.')
require(core.document_identity(doc) == document_id, 'preview_document_unavailable', 'Session document identity differs.')
return doc, design
def known_document(app, wanted):
wanted = exact_target(wanted)
doc, design = known_identity(app, wanted['documentId'])
require(core.revision(design) == wanted['revision'],
'stale_revision', 'Exact document identity/revision changed.')
return doc, design
def blank_model(design):
root = design.rootComponent
require(all(backend.count(getattr(root, field), backend.MAX_BODIES) == 0
for field in ('bRepBodies', 'meshBodies', 'occurrences', 'sketches', 'features'))
and backend.count(design.allComponents, 1) == 1 and same(design.allComponents.item(0), root)
and backend.count(design.allParameters, 0) == 0 and backend.count(design.timeline, 0) == 0
and backend.count(root.customGraphicsGroups, 0) == 0,
'preview_not_blank', 'Presentation must be a blank Design with no graphics before reservation.')
def retained_json_bytes(*objects):
"""Count every distinct retained serialized string, including duplicate content."""
seen = set()
total = 0
for obj in objects:
for key, value in vars(obj).items():
if key.endswith('_json') and isinstance(value, str) and id(value) not in seen:
seen.add(id(value))
total += len(value.encode('utf-8'))
return total
def terminal_record(history, operation_id, operation=None):
record = history.store().lookup(SESSION_ID, operation_id)
require(isinstance(record, dict) and record.get('sessionId') == SESSION_ID
and record.get('operationId') == operation_id and record.get('phase') in ('succeeded', 'failed', 'not_executed'),
'preview_history_unresolved', 'A coherent current-session terminal operation record is required.')
result = record.get('result')
require(isinstance(result, dict) and result.get('sessionId') == SESSION_ID and result.get('operationId') == operation_id
and type(result.get('success')) is bool and result['success'] == (record['phase'] == 'succeeded'),
'preview_history_unresolved', 'Terminal record identity/result is incoherent.')
if operation is not None:
require(record.get('operation') == operation and result.get('operation') == operation,
'preview_ownership', 'Document creation proof is not the required operation.')
return record
class Controller:
def __init__(self):
self.epoch = str(uuid.uuid4())
self.entries = {}
self.claims = {}
def guard(self, args):
backend.main_thread()
require(args.get('sessionId') == SESSION_ID, 'session_mismatch', 'Preview control requires the current session.')
def entry(self, app, args):
self.guard(args)
key = bounded_text(args.get('previewId'), 'previewId')
entry = self.entries.get(key)
require(entry is not None and entry['epoch'] == self.epoch and entry['sessionId'] == SESSION_ID,
'preview_session_expired', 'No live controller capability identifies this preview; history does not rebind native resources.')
require(same(entry['app'], app), 'preview_ownership', 'Preview belongs to another application.')
return entry
def ownership(self, entry):
backend.main_thread()
require(entry['epoch'] == self.epoch and entry['sessionId'] == SESSION_ID
and self.entries.get(entry['previewId']) is entry
and self.claims.get(entry['target']['documentId']) == entry['previewId'],
'preview_ownership', 'Exclusive live presentation lease is unavailable.')
doc, design = known_identity(entry['app'], entry['target']['documentId'])
require(same(doc, entry['document']) and same(design, entry['design']) and doc.isSaved is False
and doc.creationId == entry['creationId'],
'preview_ownership', 'Retained native presentation identity differs.')
return {'success': True, 'ownerId': entry['previewId'], 'sessionId': SESSION_ID,
'documentId': entry['target']['documentId']}
def lease(self, entry, cleanup=False):
if entry['lease'] is None:
entry['lease'] = backend.OwnedPresentationLease(entry['app'], entry['document'], entry['design'],
entry['previewId'], lambda: self.ownership(entry), expected_state=entry['initialPresentationState'])
entry['lease'].check(cleanup=cleanup)
return entry['lease']
def reserve(self, app, args, history):
self.guard(args)
wanted = exact_target(args.get('target'))
source = exact_target(args.get('sourceTarget'))
require(wanted['documentId'] != source['documentId'], 'preview_ownership', 'Source and presentation must be distinct documents.')
creation_id = bounded_text(args.get('creationOperationId'), 'creationOperationId')
record = terminal_record(history, creation_id, 'new_design')
result = record['result']
data = result.get('data')
require(record['phase'] == 'succeeded' and isinstance(data, dict) and data.get('target') == wanted
and isinstance(data.get('document'), dict) and data['document'].get('saved') is False,
'preview_ownership', 'Committed new_design result does not identify this unsaved presentation target.')
creation = bounded_text(data['document'].get('creationId'), 'creationId', 256)
previous = [item for item in self.entries.values() if item['target']['documentId'] == wanted['documentId']]
predecessor = previous[-1] if previous else None
if predecessor is None:
doc, design = known_document(app, wanted) # Initial claim remains strict against real new_design.
else:
require(predecessor['state'] == 'disposed' and self.last_history(predecessor, history)['terminalRecorded']
and predecessor['priorUnresolved'] is None and predecessor['finalPresentationState'] is not None,
'preview_history_unresolved', 'Previous presentation use lacks coherent durable verified disposal/release evidence.')
require(predecessor['creationOperationId'] == creation_id and predecessor['creationDigest'] == record['requestDigest']
and predecessor['target'] == wanted and predecessor['epoch'] == self.epoch,
'preview_ownership', 'Reuse must preserve its exact original creation proof and controller epoch.')
doc, design = known_identity(app, wanted['documentId'])
require(same(doc, predecessor['document']) and same(design, predecessor['design'])
and same(design.rootComponent, predecessor['lease'].root_component),
'preview_ownership', 'Disposed presentation native identity changed.')
require(same(app.activeDocument, doc) and doc.creationId == creation and doc.isSaved is False,
'preview_ownership', 'Claim requires the exact active unsaved document returned by new_design.')
blank_model(design)
initial_state = backend.presentation_observation(doc, design)
require(initial_state['revision'] == wanted['revision'] if predecessor is None
else initial_state == predecessor['finalPresentationState'],
'stale_revision', 'Presentation changed outside the verified creation/disposal chain.')
source_doc, source_design = known_document(app, source)
source_flags = flags(source_doc)
require(wanted['documentId'] not in self.claims, 'preview_ownership', 'Presentation already has an exclusive preview lease.')
require(len(self.entries) < MAX_ENTRIES and len(self.claims) < MAX_ACTIVE
and sum(e['planBytes'] for e in self.entries.values()) + MAX_PLAN_BYTES <= MAX_TOTAL_PLAN_BYTES
and sum(e['bodyBudget'] for e in self.entries.values()) + backend.MAX_BODIES <= MAX_TOTAL_BODIES,
'preview_capacity', 'Session preview capacity is full; retained resources are never evicted.')
key = self.epoch + ':' + str(uuid.uuid4())
entry = {'previewId': key, 'epoch': self.epoch, 'sessionId': SESSION_ID, 'app': app,
'document': doc, 'design': design, 'target': wanted, 'sourceTarget': source,
'sourceDocument': source_doc, 'sourceDesign': source_design, 'sourceFlags': source_flags,
'creationOperationId': creation_id, 'creationDigest': record['requestDigest'], 'creationId': creation,
'generation': 0, 'state': 'reserved', 'lease': None, 'backend': None,
'initialPresentationState': initial_state, 'finalPresentationState': None,
'predecessorPreviewId': predecessor['previewId'] if predecessor is not None else None,
'planBytes': MAX_PLAN_BYTES, 'bodyBudget': backend.MAX_BODIES,
'planId': None, 'samplesDigest': None, 'sampleCount': None, 'sampleIndex': None,
'activationIssued': False, 'factoryMayHaveBeenIssued': False, 'nativeWorkInFlight': False,
'lastOperationId': args['operationId'], 'lastOperation': args['op'], 'lastRequestDigest': fingerprint(args), 'lastReturned': False,
'lastExpectedResultDigest': None,
'lastObserved': None, 'priorUnresolved': None, 'disposition': None,
'disposalAttempted': False, 'disposalOutcome': 'not_attempted'}
self.entries[key] = entry
self.claims[wanted['documentId']] = key
return entry
def begin(self, entry, args):
require(not entry['nativeWorkInFlight'], 'preview_in_progress', 'A native preview invocation is still in flight.')
entry['generation'] += 1
entry['lastOperationId'] = args['operationId']
entry['lastOperation'] = args['op']
entry['lastExpectedResultDigest'] = None
entry['lastRequestDigest'] = fingerprint(args)
entry['lastReturned'] = False
entry['nativeWorkInFlight'] = True
def last_history(self, entry, history):
if entry['lastOperationId'] is None:
return {'terminalRecorded': False, 'reason': 'no_effect_operation'}
try:
record = terminal_record(history, entry['lastOperationId'])
require(record.get('requestDigest') == entry['lastRequestDigest']
and record['phase'] in ('succeeded', 'failed') and entry['lastExpectedResultDigest'] is not None
and core.digest(record['result']) == entry['lastExpectedResultDigest'],
'preview_history_unresolved', 'Last operation digest or observed terminal result differs.')
return {'terminalRecorded': True, 'phase': record['phase'], 'operationId': entry['lastOperationId'],
'requestDigest': entry['lastRequestDigest']}
except Exception as error:
return {'terminalRecorded': False, 'operationId': entry['lastOperationId'],
'requestDigest': entry['lastRequestDigest'], 'errorCode': getattr(error, 'code', 'preview_history_unresolved'),
'error': str(error)[:512]}
def existing(self, app, args, history, disposal=False):
entry = self.entry(app, args)
require(exact_target(args.get('target')) == entry['target'], 'preview_ownership', 'Presentation target differs from its lease.')
require(type(args.get('expectedGeneration')) is int and args['expectedGeneration'] == entry['generation'],
'preview_generation_conflict', 'Preview generation changed; inspect before another intent.',
currentGeneration=entry['generation'])
require(entry['state'] != 'disposed' and not entry['nativeWorkInFlight'], 'preview_state', 'Preview is disposed or in flight.')
if disposal:
require(not entry['disposalAttempted'], 'preview_disposal_unresolved',
'A prior deletion attempt remains unresolved; a new operation ID cannot authorize another delete.')
self.lease(entry, cleanup=disposal)
previous = self.last_history(entry, history)
if not previous['terminalRecorded']:
require(disposal and entry['lastReturned'] and entry['lastObserved'] is not None
and entry['backend'] is not None and entry['backend'].root is not None
and entry['backend'].state in ('ready', 'retained_failure'),
'preview_history_unresolved', 'Prior operation history is unresolved; automatic continuation is disabled.',
priorOperation=previous)
entry['backend']._ownership() # Explicit cleanup exception, never adoption.
previous['observedResultDigest'] = core.digest(entry['lastObserved'])
if not disposal:
require(entry['state'] == 'ready', 'preview_state', 'Only a verified ready preview can be sampled or shown.')
return entry, None if previous['terminalRecorded'] else previous
def source_check(self, entry):
doc, design = known_document(entry['app'], entry['sourceTarget'])
require(same(doc, entry['sourceDocument']) and same(design, entry['sourceDesign'])
and flags(doc) == entry['sourceFlags'], 'preview_source_changed', 'Source identity/revision/flags changed.')
def compact(self, entry, scope='operation_completion'):
return {'kind': 'EXPLODED_PREVIEW', 'receiptScope': scope, 'controllerSchemaVersion': SCHEMA_VERSION,
'backendSourceDigest': BACKEND_SOURCE_DIGEST, 'previewId': entry['previewId'], 'controllerEpoch': self.epoch,
'sessionId': SESSION_ID, 'presentationTarget': copy.deepcopy(entry['target']),
'sourceTarget': copy.deepcopy(entry['sourceTarget']), 'creationOperationId': entry['creationOperationId'],
'presentationTargetScope': 'immutable_creation_anchor',
'lastVerifiedPresentationRevision': entry['lease'].model_revision if entry['lease'] is not None else entry['initialPresentationState']['revision'],
'presentationVerificationSequence': entry['lease'].sequence if entry['lease'] is not None else 0,
'presentationFailureObservation': copy.deepcopy(entry['lease'].failure_observation) if entry['lease'] is not None else None,
'predecessorPreviewId': entry['predecessorPreviewId'],
'observedEmptyTarget': {'documentId': entry['target']['documentId'], 'revision': entry['finalPresentationState']['revision']} if entry['finalPresentationState'] is not None else None,
'planId': entry['planId'], 'samplesDigest': entry['samplesDigest'], 'sampleCount': entry['sampleCount'],
'state': entry['state'], 'generation': entry['generation'],
'sampleIndex': entry['sampleIndex'] if entry['state'] == 'ready' else None,
'lastVerifiedSampleIndex': entry['sampleIndex'],
'pose': ('sample' if entry['sampleIndex'] is not None else 'home') if entry['state'] == 'ready' else 'unverified',
'disposalAttempted': entry['disposalAttempted'], 'disposalOutcome': entry['disposalOutcome'],
'lastOperationId': entry['lastOperationId'], 'nativeWorkInFlight': entry['nativeWorkInFlight'],
'documentDisposition': 'open_for_caller', 'disposition': entry['disposition'],
'sourceMoved': False, 'sourceVisibilityWritten': False, 'rollbackClaimed': False,
'rendered': False, 'storyboardCreated': False, 'visualQualified': False,
'archivePersistence': 'UNVERIFIED', 'priorOperationUnresolved': entry['priorUnresolved'] is not None,
'priorUnresolvedOperation': copy.deepcopy(entry['priorUnresolved'])}
def release(self, entry, disposition):
require(self.claims.get(entry['target']['documentId']) == entry['previewId'], 'preview_ownership', 'Exclusive claim changed.')
require(entry['lease'] is not None, 'preview_ownership', 'Verified presentation lease is required for release.')
entry['finalPresentationState'] = copy.deepcopy(entry['lease'].check())
del self.claims[entry['target']['documentId']]
entry.update(state='disposed', disposition=disposition, planBytes=0, bodyBudget=0)
entry['backend'] = None # Drop large stored samples/geometry; retain bounded tombstone/native doc proof.
def no_graphics_release(self, entry):
require(not entry['factoryMayHaveBeenIssued'], 'preview_reconciliation_required', 'Graphics creation may have been issued.')
self.lease(entry)
require(backend.count(entry['design'].rootComponent.customGraphicsGroups, 0) == 0,
'preview_reconciliation_required', 'No-graphics absence was not verified.')
self.release(entry, 'reservation_released_no_graphics')
def failure(self, entry, error):
entry['state'] = 'retained_failure'
recovery_error = None
if not entry['factoryMayHaveBeenIssued']:
try:
self.no_graphics_release(entry)
except Exception as cleanup_error:
recovery_error = str(cleanup_error)[:512]
observed = {'errorCode': getattr(error, 'code', 'preview_native_failed'), 'error': str(error)[:2048],
'effectState': 'possible_graphics' if entry['factoryMayHaveBeenIssued'] else 'activation_only' if entry['activationIssued'] else 'none',
'preview': self.compact(entry), 'recoveryError': recovery_error}
observed['preview']['nativeWorkInFlight'] = False # Synchronous native effect already returned/threw.
entry['lastObserved'] = observed
entry['lastExpectedResultDigest'] = core.digest({
'success': False, 'sessionId': SESSION_ID, 'operationId': entry['lastOperationId'],
'errorCode': observed['errorCode'], 'error': observed['error'], 'effectState': observed['effectState'],
'preview': observed['preview'], 'recoveryError': recovery_error, 'rollbackClaimed': False})
raise MechanicalError(observed['errorCode'], observed['error'], effectState=observed['effectState'],
preview=observed['preview'], recoveryError=recovery_error, rollbackClaimed=False) from error
def create(self, app, args, history):
self.guard(args)
bounded_text(args.get('operationId'), 'operationId')
bounded_text(args.get('expectedPlanId'), 'expectedPlanId', 64)
planner = copy.deepcopy(args['planner'])
planner.update(op='exploded_view_plan', sessionId=SESSION_ID, target=exact_target(args['sourceTarget']))
# Validate pure planner mathematics before reserving resources/activation.
request_arguments(planner)
entry = self.reserve(app, args, history)
try:
self.lease(entry)
self.source_check(entry)
self.begin(entry, args)
entry['state'] = 'creating'
entry['activationIssued'] = True
require(entry['sourceDocument'].activate() is True and same(app.activeDocument, entry['sourceDocument']),
'preview_activation_refused', 'Exact source activation was not acknowledged.')
self.source_check(entry)
prepared = backend.prepare(app, planner, args['expectedPlanId'])
preview = backend.Preview(prepared, entry['lease'])
entry['backend'] = preview # Retain before any subsequent failure or graphics effect.
byte_count = retained_json_bytes(prepared, preview)
require(byte_count <= MAX_PLAN_BYTES, 'preview_capacity', 'Total retained plan/sample JSON exceeds its reserved memory bound.')
entry.update(planId=prepared.plan_id, samplesDigest=preview.samples_digest,
sampleCount=len(json.loads(preview._sample_json)), planBytes=byte_count)
entry['factoryMayHaveBeenIssued'] = True
preview.create()
self.source_check(entry)
entry.update(state='ready', bodyBudget=sum(len(n['graphics']) for n in preview.nodes))
result = self.compact(entry)
result['nativeWorkInFlight'] = False
entry['lastObserved'] = copy.deepcopy(result)
entry['lastExpectedResultDigest'] = core.digest({
'success': True, 'sessionId': SESSION_ID, 'operationId': entry['lastOperationId'],
'operation': entry['lastOperation'], 'data': result})
return result
except Exception as error:
self.failure(entry, error)
finally:
entry['nativeWorkInFlight'] = False
entry['lastReturned'] = True
def action(self, action, app, args, history):
self.guard(args)
bounded_text(args.get('operationId'), 'operationId')
if action == 'sample':
require(type(args.get('sampleIndex')) is int and args['sampleIndex'] >= 0,
'invalid_input', 'Sample index must be a nonnegative integer.')
entry, unresolved = self.existing(app, args, history, disposal=action == 'dispose')
preview = entry['backend']
if action == 'sample':
require(args['sampleIndex'] < entry['sampleCount'], 'invalid_input', 'Sample index exceeds the immutable stored samples.')
if action != 'dispose':
self.source_check(entry)
preview._guard(); preview._graphics()
elif preview is not None:
preview._ownership()
if action == 'show':
require(any(same(app.activeDocument, doc) for doc in (entry['sourceDocument'], entry['document'])),
'preview_activation_refused', 'Another active document intervened; no activation performed.')
self.begin(entry, args)
entry['priorUnresolved'] = unresolved
entry['state'] = 'updating'
try:
if action == 'sample':
preview.sample(args['sampleIndex'])
entry['sampleIndex'] = args['sampleIndex']
self.source_check(entry)
entry['state'] = 'ready'
elif action == 'show':
preview.activate()
self.source_check(entry)
entry['state'] = 'ready'
elif action == 'dispose':
if preview is None:
self.no_graphics_release(entry)
else:
entry['disposalAttempted'] = True # Before native deletion; never retry uncertain effects.
entry['disposalOutcome'] = 'unknown'
preview.dispose()
entry['disposalOutcome'] = 'verified_absent'
self.release(entry, 'graphics_disposed')
result = self.compact(entry)
result['nativeWorkInFlight'] = False
entry['lastObserved'] = copy.deepcopy(result)
entry['lastExpectedResultDigest'] = core.digest({
'success': True, 'sessionId': SESSION_ID, 'operationId': entry['lastOperationId'],
'operation': entry['lastOperation'], 'data': result})
return result
except Exception as error:
self.failure(entry, error)
finally:
entry['nativeWorkInFlight'] = False
entry['lastReturned'] = True
def inspect(self, app, args, history):
entry = self.entry(app, args)
result = self.compact(entry, 'live_observation')
result['observedAt'] = time.time()
result['history'] = self.last_history(entry, history)
result['usable'] = False
result['lastKnownDocumentDisposition'] = result['documentDisposition']
result['documentDisposition'] = 'unknown_current'
errors = []
if entry['state'] == 'disposed':
result['receiptScope'] = 'registry_tombstone'
result['nativeResourceRights'] = False
result['nativeObservation'] = 'NOT_PERFORMED'
result['observationComplete'] = False
return result
try:
self.lease(entry, cleanup=entry['state'] == 'retained_failure')
result['presentationOwnershipVerified'] = True
result['documentDisposition'] = 'verified_open_owned'
preview = entry['backend']
if preview is not None:
preview._ownership()
result['graphicsOwnershipVerified'] = True
preview._graphics()
result['graphicsReadbackVerified'] = True
else:
result['graphicsCount'] = backend.count(entry['design'].rootComponent.customGraphicsGroups, 1)
except Exception as error:
errors.append({'scope': 'presentation', 'code': getattr(error, 'code', 'preview_read_failed'), 'message': str(error)[:512]})
try:
self.source_check(entry)
if entry['backend'] is not None:
entry['backend'].prepared.check()
result['sourceUnchanged'] = True
except Exception as error:
errors.append({'scope': 'source', 'code': getattr(error, 'code', 'preview_read_failed'), 'message': str(error)[:512]})
result['errors'] = errors
result['observationComplete'] = not errors
result['usable'] = not errors and entry['state'] == 'ready' and result['history']['terminalRecorded'] and not entry['nativeWorkInFlight']
return result
_controller = Controller()
def create(app, args, history):
return _controller.create(app, args, history)
def sample(app, args, history):
return _controller.action('sample', app, args, history)
def show(app, args, history):
return _controller.action('show', app, args, history)
def dispose(app, args, history):
return _controller.action('dispose', app, args, history)
def inspect(app, args, history):
return _controller.inspect(app, args, history)
WRITES = {'exploded_preview_create': create, 'exploded_preview_sample': sample,
'exploded_preview_show': show, 'exploded_preview_dispose': dispose}
READS = {'exploded_preview_inspect': inspect}