Пример #1
0
def test_assets_consumed(publisher_ocean_instance, consumer_ocean_instance):
    ocn = publisher_ocean_instance
    acct = consumer_ocean_instance.main_account
    consumed_assets = len(ocn.assets.consumer_assets(acct.address))
    asset = create_asset(publisher_ocean_instance)
    service = asset.get_service(service_type=ServiceTypes.ASSET_ACCESS)
    service_dict = service.as_dictionary()
    sa = ServiceAgreement.from_json(service_dict)
    keeper = ocn.keeper

    def grant_access(_, ocn_instance, agr_id, did, cons_address, account):
        ocn_instance.agreements.conditions.grant_access(
            agr_id, did, cons_address, account)

    agreement_id = consumer_ocean_instance.assets.order(
        asset.did, sa.index, acct)
    keeper.lock_reward_condition.subscribe_condition_fulfilled(
        agreement_id,
        15,
        grant_access, (publisher_ocean_instance, agreement_id, asset.did,
                       acct.address, publisher_ocean_instance.main_account),
        wait=True)

    keeper.access_secret_store_condition.subscribe_condition_fulfilled(
        agreement_id,
        15,
        log_event(keeper.access_secret_store_condition.FULFILLED_EVENT), (),
        wait=True)
    assert ocn.agreements.is_access_granted(agreement_id, asset.did,
                                            acct.address)

    assert len(ocn.assets.consumer_assets(acct.address)) == consumed_assets + 1
    publisher_ocean_instance.assets.retire(asset.did)
def test_agreement_hash():
    """
    This test verifies generating agreement hash using fixed inputs and ddo example.
    This will make it easier to compare the hash generated from different languages.
    """
    did = "did:op:0bc278fee025464f8012b811d1bce8e22094d0984e4e49139df5d5ff7a028bdf"
    template_id = '0x34219511357c43f4af90a4896b972a32809379bfbe8148eab3878e8406e75836'
    agreement_id = '0xf136d6fadecb48fdb2fc1fb420f5a5d1c32d22d9424e47ab9461556e058fefaa'
    ddo = get_ddo_sample()

    sa = ServiceAgreement.from_json(
        ddo.get_service(ServiceTypes.ASSET_ACCESS).as_dictionary())
    assert did == ddo.did
    # Don't generate condition ids, use fixed ids so we get consistent hash
    # (access_id, lock_id, escrow_id) = sa.generate_agreement_condition_ids(
    #     agreement_id, ddo.asset_id, consumer, publisher, keeper)
    access_id = '0x2d7c1d60dc0c3f52aa9bd71ffdbe434a0e58435571e64c893bc9646fea7f6ec1'
    lock_id = '0x1e265c434c14e668695dda1555088f0ea4356f596bdecb8058812e7dcba9ee73'
    escrow_id = '0xe487fa6d435c2f09ef14b65b34521302f1532ac82ba8f6c86116acd8566e2da3'
    print(f'condition ids: \n'
          f'{access_id} \n'
          f'{lock_id} \n'
          f'{escrow_id}')
    agreement_hash = ServiceAgreement.generate_service_agreement_hash(
        template_id,
        (access_id, lock_id, escrow_id),
        sa.conditions_timelocks,
        sa.conditions_timeouts,
        agreement_id,
        generate_multi_value_hash
    )
    print('agreement hash: ', agreement_hash.hex())
    expected = '0xc1bc6cdb161b9d04d5b48bceba51b9092c4da8db08003b96b93647fbe86e330c'
    print('expected hash: ', expected)
    assert agreement_hash.hex() == expected, 'hash does not match.'
Пример #3
0
    def _read_dict(self, dictionary):
        """Import a JSON dict into this DDO."""
        values = copy.deepcopy(dictionary)
        self._did = values.pop('id')
        self._created = values.pop('created', None)

        if 'publicKey' in values:
            self._public_keys = []
            for value in values.pop('publicKey'):
                if isinstance(value, str):
                    value = json.loads(value)
                self._public_keys.append(DDO.create_public_key_from_json(value))
        if 'authentication' in values:
            self._authentications = []
            for value in values.pop('authentication'):
                if isinstance(value, str):
                    value = json.loads(value)
                self._authentications.append(DDO.create_authentication_from_json(value))
        if 'service' in values:
            self._services = []
            for value in values.pop('service'):
                if isinstance(value, str):
                    value = json.loads(value)

                if value['type'] == ServiceTypes.ASSET_ACCESS:
                    service = ServiceAgreement.from_json(value)
                elif value['type'] == ServiceTypes.CLOUD_COMPUTE:
                    service = ServiceAgreement.from_json(value)
                else:
                    service = Service.from_json(value)

                self._services.append(service)
        if 'proof' in values:
            self._proof = values.pop('proof')

        self._other_values = values
Пример #4
0
def test_market_flow():
    pub_wallet = get_publisher_wallet()

    publisher_ocean = get_publisher_ocean_instance()
    consumer_ocean = get_consumer_ocean_instance()

    # Register Asset
    asset = get_registered_ddo(publisher_ocean, pub_wallet)
    assert isinstance(asset, Asset)
    assert asset.data_token_address

    consumer_wallet = get_consumer_wallet()

    service = asset.get_service(service_type=ServiceTypes.ASSET_ACCESS)
    sa = ServiceAgreement.from_json(service.as_dictionary())

    # Mint data tokens and assign to publisher
    dt = publisher_ocean.get_data_token(asset.data_token_address)
    mint_tokens_and_wait(dt, pub_wallet.address, pub_wallet)

    ######
    # Give the consumer some datatokens so they can order the service
    try:
        tx_id = dt.transfer_tokens(consumer_wallet.address, 10, pub_wallet)
        dt.verify_transfer_tx(tx_id, pub_wallet.address,
                              consumer_wallet.address)
    except (AssertionError, Exception) as e:
        print(e)
        raise

    ######
    # Place order for the download service
    order_requirements = consumer_ocean.assets.order(asset.did,
                                                     consumer_wallet.address,
                                                     sa.index)

    ######
    # Pay for the service
    _order_tx_id = consumer_ocean.assets.pay_for_service(
        order_requirements.amount, order_requirements.data_token_address,
        asset.did, service.index, '0xF9f2DB837b3db03Be72252fAeD2f6E0b73E428b9',
        consumer_wallet)
    ######
    # Download the asset files
    asset_folder = consumer_ocean.assets.download(
        asset.did, sa.index, consumer_wallet, _order_tx_id,
        consumer_ocean.config.downloads_path)

    assert len(os.listdir(asset_folder)) > 1

    orders = consumer_ocean.get_user_orders(consumer_wallet.address,
                                            asset.asset_id)
    assert orders, f'no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}'

    orders = consumer_ocean.get_user_orders(
        consumer_wallet.address,
        consumer_ocean.web3.toChecksumAddress(asset.asset_id))
    assert orders, f'no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}'

    orders = consumer_ocean.get_user_orders(consumer_wallet.address)
    assert orders, f'no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}'
Пример #5
0
def test_market_flow(order_type):
    """Tests that an order is correctly placed on the market.

    The parameter implicit_none sends the payload with an empty key as the delegated consumer.
    The parameter explicit_none sends None as the delegated consumer, explicitly."""
    pub_wallet = get_publisher_wallet()

    publisher_ocean = get_publisher_ocean_instance()
    consumer_ocean = get_consumer_ocean_instance()

    # Register Asset
    asset = get_registered_ddo(publisher_ocean, get_metadata(), pub_wallet)
    assert isinstance(asset, Asset)
    assert asset.data_token_address

    consumer_wallet = get_consumer_wallet()

    service = asset.get_service(service_type=ServiceTypes.ASSET_ACCESS)
    sa = ServiceAgreement.from_json(service.as_dictionary())

    # Mint data tokens and assign to publisher
    dt = publisher_ocean.get_data_token(asset.data_token_address)
    mint_tokens_and_wait(dt, pub_wallet.address, pub_wallet)

    ######
    # Give the consumer some datatokens so they can order the service
    try:
        tx_id = dt.transfer_tokens(consumer_wallet.address, 10.0, pub_wallet)
        dt.verify_transfer_tx(tx_id, pub_wallet.address,
                              consumer_wallet.address)
    except (AssertionError, Exception) as e:
        print(e)
        raise

    ######
    # Place order for the download service
    order_requirements = consumer_ocean.assets.order(asset.did,
                                                     consumer_wallet.address,
                                                     sa.index)

    ######
    # Pay for the service
    args = [
        order_requirements.amount,
        order_requirements.data_token_address,
        asset.did,
        service.index,
        "0xF9f2DB837b3db03Be72252fAeD2f6E0b73E428b9",
        consumer_wallet,
    ]

    if order_type == "explicit_none":
        args.append(None)

    _order_tx_id = consumer_ocean.assets.pay_for_service(*args)

    ######
    # Download the asset files
    asset_folder = consumer_ocean.assets.download(
        asset.did,
        sa.index,
        consumer_wallet,
        _order_tx_id,
        consumer_ocean.config.downloads_path,
    )

    assert len(os.listdir(asset_folder)) >= 1

    if order_type == "explicit_none":
        # no need to continue, order worked
        return

    orders = consumer_ocean.get_user_orders(consumer_wallet.address,
                                            asset.asset_id)
    assert (
        orders
    ), f"no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}"

    orders = consumer_ocean.get_user_orders(
        consumer_wallet.address,
        consumer_ocean.web3.toChecksumAddress(asset.asset_id))
    assert (
        orders
    ), f"no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}"

    orders = consumer_ocean.get_user_orders(consumer_wallet.address)
    assert (
        orders
    ), f"no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}"
Пример #6
0
def test_payer_market_flow():
    """Tests that an order can be placed for a delegated consumer, other than the payer."""
    pub_wallet = get_publisher_wallet()

    publisher_ocean = get_publisher_ocean_instance()
    consumer_ocean = get_consumer_ocean_instance()
    another_consumer_ocean = get_another_consumer_ocean_instance(
        use_provider_mock=True)

    # Register Asset
    asset = get_registered_ddo(publisher_ocean, get_metadata(), pub_wallet)
    assert isinstance(asset, Asset)
    assert asset.data_token_address

    another_consumer_wallet = get_another_consumer_wallet()
    consumer_wallet = get_consumer_wallet()

    service = asset.get_service(service_type=ServiceTypes.ASSET_ACCESS)
    sa = ServiceAgreement.from_json(service.as_dictionary())

    # Mint data tokens and assign to publisher
    dt = publisher_ocean.get_data_token(asset.data_token_address)
    mint_tokens_and_wait(dt, pub_wallet.address, pub_wallet)

    ######
    # Give the consumer some datatokens so they can order the service
    try:
        tx_id = dt.transfer_tokens(consumer_wallet.address, 10.0, pub_wallet)
        dt.verify_transfer_tx(tx_id, pub_wallet.address,
                              consumer_wallet.address)
    except (AssertionError, Exception) as e:
        print(e)
        raise

    ######
    # Place order for the download service
    order_requirements = consumer_ocean.assets.order(
        asset.did, another_consumer_wallet.address, sa.index)

    ######
    # Pay for the service and have another_consumer_wallet as consumer
    _order_tx_id = consumer_ocean.assets.pay_for_service(
        order_requirements.amount,
        order_requirements.data_token_address,
        asset.did,
        service.index,
        "0xF9f2DB837b3db03Be72252fAeD2f6E0b73E428b9",
        consumer_wallet,
        another_consumer_wallet.address,
    )
    asset_folder = None
    assert asset_folder is None
    if asset_folder is None:
        # Download the asset files
        asset_folder = another_consumer_ocean.assets.download(
            asset.did,
            sa.index,
            another_consumer_wallet,
            _order_tx_id,
            another_consumer_ocean.config.downloads_path,
        )
    assert len(os.listdir(asset_folder)) >= 1

    orders = consumer_ocean.get_user_orders(consumer_wallet.address,
                                            asset.asset_id)
    assert (
        orders
    ), f"no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}"

    orders = consumer_ocean.get_user_orders(
        consumer_wallet.address,
        consumer_ocean.web3.toChecksumAddress(asset.asset_id))
    assert (
        orders
    ), f"no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}"

    orders = consumer_ocean.get_user_orders(consumer_wallet.address)
    assert (
        orders
    ), f"no orders found using the order history: datatoken {asset.asset_id}, consumer {consumer_wallet.address}"
Пример #7
0
def buy_asset():
    """
    Requires all ocean services running.

    """
    setup_logging(default_level=logging.INFO)
    ConfigProvider.set_config(ExampleConfig.get_config())
    config = ConfigProvider.get_config()
    providers = {
        'duero': '0xfEF2d5e1670342b9EF22eeeDcb287EC526B48095',
        'nile': '0x4aaab179035dc57b35e2ce066919048686f82972'
    }
    # make ocean instance
    ocn = Ocean()
    acc = get_account(1)
    if not acc:
        acc = ([acc for acc in ocn.accounts.list() if acc.password] or ocn.accounts.list())[0]

    Diagnostics.verify_contracts()
    metadata = example_metadata.metadata.copy()
    metadata['main']['dateCreated'] = get_timestamp()
    keeper = Keeper.get_instance()
    # Register ddo
    did = ''
    if did:
        ddo = ocn.assets.resolve(did)
        logging.info(f'using ddo: {did}')
    else:

        ddo = ocn.assets.create(metadata, acc, providers=[], use_secret_store=True)
        assert ddo is not None, f'Registering asset on-chain failed.'
        did = ddo.did
        logging.info(f'registered ddo: {did}')
        # ocn here will be used only to publish the asset. Handling the asset by the publisher
        # will be performed by the Brizo server running locally
        test_net = os.environ.get('TEST_NET', '')
        if test_net.startswith('nile'):
            provider = keeper.did_registry.to_checksum_address(providers['nile'])
        elif test_net.startswith('duero'):
            provider = keeper.did_registry.to_checksum_address(providers['duero'])
        else:
            provider = '0x068Ed00cF0441e4829D9784fCBe7b9e26D4BD8d0'

        # Wait for did registry event
        event = keeper.did_registry.subscribe_to_event(
            keeper.did_registry.DID_REGISTRY_EVENT_NAME,
            30,
            event_filter={
                '_did': Web3Provider.get_web3().toBytes(hexstr=ddo.asset_id),
                '_owner': acc.address},
            wait=True
        )
        if not event:
            logging.warning(f'Failed to get the did registry event for asset with did {did}.')
        assert keeper.did_registry.get_block_number_updated(ddo.asset_id) > 0, \
            f'There is an issue in registering asset {did} on-chain.'

        keeper.did_registry.add_provider(ddo.asset_id, provider, acc)
        logging.info(f'is {provider} set as did provider: '
                     f'{keeper.did_registry.is_did_provider(ddo.asset_id, provider)}')

    _providers = keeper.did_registry.get_did_providers(ddo.asset_id)

    access_template_name = keeper.template_manager.SERVICE_TO_TEMPLATE_NAME['access']
    template_id = keeper.template_manager.create_template_id(access_template_name)
    approved = keeper.template_manager.is_template_approved(template_id)
    print(f'agreement template approved: {approved}')
    template_data = keeper.template_manager.get_template(template_id)
    print(f'access agreement template: {template_data}')
    cons_ocn = Ocean()
    consumer_account = get_account(0)

    # sign agreement using the registered asset did above
    service = ddo.get_service(service_type=ServiceTypes.ASSET_ACCESS)
    # This will send the order request to Brizo which in turn will execute the agreement on-chain
    cons_ocn.accounts.request_tokens(consumer_account, 10)
    sa = ServiceAgreement.from_json(service.as_dictionary())
    agreement_id = ''
    if not agreement_id:
        agreement_id = cons_ocn.assets.order(
            did, sa.index, consumer_account)

    logging.info('placed order: %s, %s', did, agreement_id)
    event = keeper.agreement_manager.subscribe_agreement_created(
        agreement_id, 60, None, (), wait=True
    )
    assert event, "Agreement event is not found, check the keeper node's logs"
    logging.info(f'Got agreement event, next: lock reward condition')

    event = keeper.lock_reward_condition.subscribe_condition_fulfilled(
        agreement_id, 60, None, (), wait=True
    )
    assert event, "Lock reward condition fulfilled event is not found, check the keeper node's logs"
    logging.info('Got lock reward event, next: wait for the access condition..')

    event = keeper.access_secret_store_condition.subscribe_condition_fulfilled(
        agreement_id, 15, None, (), wait=True
    )
    logging.info(f'Got access event {event}')
    i = 0
    while ocn.agreements.is_access_granted(
            agreement_id, did, consumer_account.address) is not True and i < 30:
        time.sleep(1)
        i += 1

    assert ocn.agreements.is_access_granted(agreement_id, did, consumer_account.address)

    ocn.assets.consume(
        agreement_id,
        did,
        sa.index,
        consumer_account,
        config.downloads_path)
    logging.info('Success buying asset.')

    event = keeper.escrow_reward_condition.subscribe_condition_fulfilled(
        agreement_id,
        30,
        None,
        (),
        wait=True
    )
    assert event, 'no event for EscrowReward.Fulfilled'
    logging.info(f'got EscrowReward.FULFILLED event: {event}')
    logging.info('Done buy asset.')
Пример #8
0
def test_buy_asset(consumer_ocean_instance, publisher_ocean_instance):
    config = ExampleConfig.get_config()
    ConfigProvider.set_config(config)
    keeper = Keeper.get_instance()
    # :TODO: enable the actual SecretStore
    # SecretStoreProvider.set_secret_store_class(SecretStore)
    w3 = Web3Provider.get_web3()
    pub_acc = get_publisher_account()

    # Register ddo
    ddo = get_registered_ddo(publisher_ocean_instance, pub_acc)
    assert isinstance(ddo, DDO)
    # ocn here will be used only to publish the asset. Handling the asset by the publisher
    # will be performed by the Brizo server running locally

    cons_ocn = consumer_ocean_instance
    # restore the http client because we want the actual Brizo server to do the work
    # not the BrizoMock.
    # Brizo.set_http_client(requests)
    consumer_account = get_consumer_account()

    downloads_path_elements = len(
        os.listdir(
            consumer_ocean_instance._config.downloads_path)) if os.path.exists(
                consumer_ocean_instance._config.downloads_path) else 0
    # sign agreement using the registered asset did above
    service = ddo.get_service(service_type=ServiceTypes.ASSET_ACCESS)
    sa = ServiceAgreement.from_json(service.as_dictionary())
    # This will send the consume request to Brizo which in turn will execute the agreement on-chain
    cons_ocn.accounts.request_tokens(consumer_account, 100)
    agreement_id = cons_ocn.assets.order(ddo.did,
                                         sa.index,
                                         consumer_account,
                                         auto_consume=False)

    event_wait_time = 10
    event = keeper.escrow_access_secretstore_template.subscribe_agreement_created(
        agreement_id,
        event_wait_time,
        log_event(
            keeper.escrow_access_secretstore_template.AGREEMENT_CREATED_EVENT),
        (),
        wait=True)
    assert event, 'no event for EscrowAccessSecretStoreTemplate.AgreementCreated'

    event = keeper.lock_reward_condition.subscribe_condition_fulfilled(
        agreement_id,
        event_wait_time,
        log_event(keeper.lock_reward_condition.FULFILLED_EVENT), (),
        wait=True)
    assert event, 'no event for LockRewardCondition.Fulfilled'

    # give access
    publisher_ocean_instance.agreements.conditions.grant_access(
        agreement_id, ddo.did, consumer_account.address, pub_acc)
    event = keeper.access_secret_store_condition.subscribe_condition_fulfilled(
        agreement_id,
        event_wait_time,
        log_event(keeper.access_secret_store_condition.FULFILLED_EVENT), (),
        wait=True)
    assert event, 'no event for AccessSecretStoreCondition.Fulfilled'
    assert cons_ocn.agreements.is_access_granted(agreement_id, ddo.did,
                                                 consumer_account.address)

    assert cons_ocn.assets.consume(agreement_id, ddo.did, sa.index,
                                   consumer_account, config.downloads_path)

    assert len(os.listdir(
        config.downloads_path)) == downloads_path_elements + 1

    # Check that we can consume only an specific file in passing the index.
    assert cons_ocn.assets.consume(agreement_id, ddo.did, sa.index,
                                   consumer_account, config.downloads_path, 2)
    assert len(os.listdir(
        config.downloads_path)) == downloads_path_elements + 1

    with pytest.raises(AssertionError):
        cons_ocn.assets.consume(agreement_id, ddo.did, sa.index,
                                consumer_account, config.downloads_path, -2)

    with pytest.raises(AssertionError):
        cons_ocn.assets.consume(agreement_id, ddo.did, sa.index,
                                consumer_account, config.downloads_path, 3)

    # decrypt the contentUrls using the publisher account instead of consumer account.
    # if the secret store is working and ACL check is enabled, this should fail
    # since SecretStore decrypt will fail the checkPermissions check
    try:
        cons_ocn.assets.consume(agreement_id, ddo.did, sa.index, pub_acc,
                                config.downloads_path)
    except RPCError:
        print('hooray, secret store is working as expected.')

    publisher_ocean_instance.agreements.conditions.release_reward(
        agreement_id, sa.get_price(), pub_acc)

    event = keeper.escrow_reward_condition.subscribe_condition_fulfilled(
        agreement_id,
        event_wait_time + 20,
        log_event(keeper.escrow_reward_condition.FULFILLED_EVENT), (),
        wait=True)
    assert event, 'no event for EscrowReward.Fulfilled'

    assert w3.toHex(event.args['_agreementId']) == agreement_id
Пример #9
0
    def start(
        self,
        input_datasets: list,
        consumer_wallet: Wallet,
        nonce: [int, None] = None,
        algorithm_did: [str, None] = None,
        algorithm_meta: [AlgorithmMetadata, None] = None,
        algorithm_tx_id: str = None,
        algorithm_data_token: str = None,
        output: dict = None,
        job_id: str = None,
    ):
        """
        Start a remote compute job on the asset files.

        Files are identified by `did` after verifying that the provider service is active and transferring the
        number of data-tokens required for using this compute service.

        :param input_datasets: list of ComputeInput -- list of input datasets to the compute job. A dataset is
            represented with ComputeInput struct
        :param consumer_wallet: Wallet instance of the consumer ordering the service
        :param nonce: int value to use in the signature
        :param algorithm_did: str -- the asset did (of `algorithm` type) which consist of `did:op:` and
            the assetId hex str (without `0x` prefix)
        :param algorithm_meta: `AlgorithmMetadata` instance -- metadata about the algorithm being run if
            `algorithm` is being used. This is ignored when `algorithm_did` is specified.
        :param algorithm_tx_id: transaction hash of algorithm StartOrder tx (Required when using `algorithm_did`)
        :param algorithm_data_token: datatoken address of this algorithm (Required when using `algorithm_did`)
        :param output: dict object to be used in publishing mechanism, must define
        :param job_id: str identifier of a compute job that was previously started and
            stopped (if supported by the provider's  backend)
        :return: str -- id of compute job being executed
        """
        assert (
            algorithm_did or algorithm_meta
        ), "either an algorithm did or an algorithm meta must be provided."

        for i in input_datasets:
            assert isinstance(i, ComputeInput)

        first_input = input_datasets[0]
        did = first_input.did
        order_tx_id = first_input.transfer_tx_id
        service_id = first_input.service_id

        output = OceanCompute.check_output_dict(
            output, consumer_wallet.address, data_provider=self._data_provider
        )
        asset = resolve_asset(did, metadata_store_url=self._config.aquarius_url)
        _, service_endpoint = self._get_service_endpoint(did, asset)

        service = asset.get_service_by_index(service_id)
        sa = ServiceAgreement.from_json(service.as_dictionary())
        assert (
            ServiceTypes.CLOUD_COMPUTE == sa.type
        ), "service at serviceId is not of type compute service."

        signature = self._sign_message(
            consumer_wallet,
            f"{consumer_wallet.address}{did}",
            nonce=nonce,
            service_endpoint=sa.service_endpoint,
        )

        job_info = self._data_provider.start_compute_job(
            did,
            service_endpoint,
            consumer_wallet.address,
            signature,
            sa.index,
            order_tx_id,
            algorithm_did,
            algorithm_meta,
            algorithm_tx_id,
            algorithm_data_token,
            output,
            input_datasets,
            job_id,
        )
        return job_info["jobId"]