Sign a .rbz—requiring Trimble ID log in—from a script

Here’s my revised cas_sign_in() function, all working again. Obviously this is somewhat sanitized, and I’ve left out my logging. However, I still can’t figure out why I’m not getting a .hash file (only a .susig) in my download link, at the end of the new extension posting function.

def cas_sign_in():
    """Use a POST request to sign in to the Trimble Developer Center
    using their CAS (Central Authentication Service).
    """
    destination = 'https://extensions.sketchup.com/extension/sign'
    params = {'destination': destination}
    login_url = 'https://login.sketchup.com/login/trimbleid'
    action_url = 'https://identity.trimble.com/commonauth'
    username = <your user name>
    password = <your password>

    # Initialize a requests session so we retain cookies and stay signed in.
    try:
        session = requests.session()

        # Use GET request to get the unique token from the sign in page.
        login = session.get(login_url, params=params)
        # Follow redirects to finally get the page with the complete form rendered on it.
        login = login.history[-1]
        # Find the hidden fields on the page (including the token).
        login_html = lxml.html.fromstring(login.text)
        hidden_inputs = login_html.xpath(r'//form//input[@type="hidden"]')

        # Initialize the form_data dict with the hidden form values.
        form_data = {x.attrib["name"]: x.value for x in hidden_inputs}

        # Add our login data to the dict.
        form_data['username'] = username
        form_data['user'] = username
        form_data['password'] = password
        form_data['action'] = action_url

        # Log in
        res = session.post(
            action_url,
            data=form_data,
            params={'sessionDataKey': form_data['sessionDataKey']}
            )

        # Confirm login by requesting identity info from the API
        api_me = 'https://extensions.sketchup.com/warehouse/v1.0/users/me'
        me_json = session.get(api_me).content
        me_data = json.loads(me_json)

        # The JSON contains other fields, such as an ID number, which we could
        # also use for this verification.
        if <your display name> not in me_data['displayName'].lower():
            raise Exception("Login failed. Please check login credentials.")

        return session
    except Exception as e:
        raise

And here is the revised function for uploading, signing, and downloading the .rbz using the new API—but this is not completely working. I’m posting it here in the hopes someone may have some insight. The signing API returns an ‘Extension signed successfully’ message, and gives a working download link, but for some reason, the results I get from the script are always missing the .hash file, even though they include the .susig file. Anyone have any ideas?

(N.B.: I’ve removed my logging around exception handling. You might want to add some print statements or other logging there when you use this. Also, you’ll need your signed-in session from the sign in function above in order to make these requests.)

def post_rbz(rbz_file, session):
    """Use our signed-in session to send POST requests to SketchUp's extension
    signature API. There are two POST requests required to to make this work:
    POST 1: upload the .rbz file to the upload endpoint.
    POST 2: make a request to the .rbz signing endpoint, including the new
    uniquely-generated name reference to the uploaded .rbz file.
    After posting, get the download link from the response, and then pass it
    back to our session to download it. Return the signed file.
    """
    try:
        files = {('file', open(rbz_file, 'rb'))}

        # # DEBUG: use this hook callback function to check the POST response.
        # # To use this, enable this line in the POST request:
        # # hooks={'response': print_content}
        # def print_content(r, *args, **kwargs):
        #     print(r.content)

        api_url = 'https://extensions.sketchup.com/api/v1/extension/upload'

        res = session.post(
            api_url,
            files=files,
            # hooks={'response': print_content}  # enable for debugging
        )
    except Exception as e:
        raise

    try:
        # Get the reference name that was generated for the uploaded rbz file.
        extension_rbz_unique = json.loads(res.content)["rbzFileNameUnique"]

        # Set up the signing data to submit, with references to our uploaded
        # file. If you want the results scrambled or encrypted, change False to True.

        form_data = {
            'scramble': False,
            'encrypt': False,
            'extensionRbzUnique': extension_rbz_unique,
            'extensionRbz': '<your extension name>.rbz'
        }

        signing_url = 'https://extensions.sketchup.com/api/v1/extension/sign'

        # Post to the API signing endpoint.
        res = session.post(
            signing_url,
            json=form_data
        )
    except Exception as e:
        raise

    # Parse the JSON response to get the download link.
    try:
        res_json = json.loads(res.content)
        dl_url = res_json['extension']
    except Exception as e:
        raise

    try:
        # Use the session to download the signed .rbz extension.
        temp_dir = os.path.dirname(rbz_file)
        dl_rbz = os.path.expanduser(os.path.join(temp_dir, '<your extension name>.rbz'))
        res = session.get(dl_url, stream=True)
        with open(dl_rbz, 'wb') as f:
            shutil.copyfileobj(res.raw, f)
        return dl_rbz
    except Exception as e:
        raise