Writing Scripts with the Seal Module

The Seal Platform allows you to write and execute your own custom Python code within the platform, enabling powerful custom solutions and automations.

If this is your first time using Python, then there are great reference materials online such as: https://docs.python.org/3.10/tutorial/

What if I don't know how to code?

Not to worry - Seal comes with a built in AI Copilot to write code for you. Navigate to the 'Copilot' tab on the right hand side, and start chatting with the AI to describe what you need.

The AI agent has access to your current code, so you can ask it to help you debug specific issues or add to the existing code.

Once you're happy with the code, click on 'Accept code' to use it in your Script.

Note that the generated code may not fully be fit for your purpose - we recommend checking over the results.

The Seal Module

The Seal module is a Python package for interacting programatically with the Seal platform.

It is automatically available in every Script's Python environment - you don't need to import it manually.

The Seal module consists of a collection of methods on the seal object:

Getting entities

seal.get_entity(entity_id, ?version)
seal.get_entity(ref={"id: "...", "version": "..."})

Returns the entire json blob of data for that entity. The entity id can be copied from the entity's URL in Seal - it's the final section of the url, after the entity title. Just passing an entity_id returns the latest version or draft of the entity.

The entity id can be copied from the entity's URL in Seal - it's the final section of the url, after the entity title.

To get a specific version of an entity, include the version as a second argument. You can also pass an entity ref object instead.

Since it returns the whole json blob for the entity, you can get the entity's fields via entity["fields"] , or a specific field via entity["fields"]["fieldName"] .

seal.get_entity_active_version(entity_id)

Gets the data for the active version of the given entity. If no active version is found (ie the entity has never been published), an error will be thrown.

seal.get_containing_entity()

Gets the entity the script card is embedded in.

seal.get_entities_by_title(title)

Gets (non-archived) entities by their title (string). Note that exact titles are matched by default. Since multiple entities can share the same title, this returns an array. You can also search for titles containing the search string with exact=False.

seal.search_entities(query_config)

Gets entities by searching with a query config object (dict). See the API documentation for more details on the query config schema.

Making an entity editable

seal.make_entity_editable(entity_id)

Used to make an entity editable (ie create a new draft). Returns the entire json blob of data for the new draft entity.

Reverting entity drafts

seal.revert_entities(entity_ids)

Revert multiple entity drafts to their previous published versions. All entities must be in EDITABLE status and have a previous version. Up to 500 entities can be reverted at once.

Parameters:

  • entity_ids: List of entity IDs to revert (maximum 500)

Returns an array of the reverted entities.

seal.revert_containing_entity()

Revert the entity the script is embedded in to its previous published version. The entity must be in EDITABLE status and have a previous version.

Returns an array containing the reverted entity.

Validating entities

Scripts can be used to run checks before an entity is published. A script can be added as a check on a type or template for its templates or instances.

seal.get_validating_entity()

Gets the entity a script is validating.

When a validation script is run, it is classed as passing if no errors are thrown. Any of the following methods will throw an error if the assertion is not met:

Method
Checks that

validation.assertEqual(a, b)

a == b

validation.assertNotEqual(a, b)

a != b

validation.assertTrue(x)

bool(x) is True

validation.assertFalse(x)

bool(x) is False

validation.assertIs(a, b)

a is b

validation.assertIsNot(a, b)

a is not b

validation.assertIsNone(x)

x is None

validation.assertIsNotNone(x)

x is not None

validation.assertIn(a, b)

a in b

validation.assertNotIn(a, b)

a not in b

validation.assertIsInstance(a, b)

isinstance(a, b)

validation.assertNotIsInstance(a, b)

not isinstance(a, b)

User-defined errors can also be thrown to fail a validation script.

seal.throw_error(message)

Throws an error with a user-provided message.

Archiving an entity

seal.archive_entity(entity_id, archive)

Archives or unarchives an entity. The archive parameter should be a boolean: True to archive, False to recover. Returns the updated entity data.

Converting an Instance to a Template

seal.convert_instance_to_template(entity_id)

Converts an instance entity into a template entity. The instance must be editable (a draft) to be converted. Returns the entire json blob of data for the new template entity.

Updating the status tag on an entity

seal.update_entity_status_tag(entity_id, status_tag)

Updates the status tag on an entity. The live entity must be in an editable state.

seal.update_upcoming_entity_status_tag(entity_id, status_tag)

Updates the upcoming status tag an entity will have when it is next published. The live entity must be in an editable state.

Getting the upcoming version info of an entity

seal.get_upcoming_version_info(entity_id)

Returns the upcoming version info of an entity. This includes the version and status tag.

Adding an entity to a change set

seal.add_entity_to_change_set(entity_id, change_set_index)

Used to add an entity to a pre-existing change set. The change_set_index can be found in the URL when on a change set page.

Returns information about the change set the given entity now belongs to. This information includes id, index, name, status and description.

Creating Change Sets

seal.create_change_set(entity_ids, ?change_set_name)

Creates a new change set containing the provided entities. Returns information about the created change set including id, index, name, status, description and entityRefs.

Getting tags

seal.get_tags_for_entity(entity_id)

Gets the tags on an entity.

seal.get_tags(tag)

Gets the tags on an entity the script is embedded in. Can only be run from a script card.

Adding tags

seal.add_tag_to_entity(entity_id, tag)

Adds a tag to an entity. If no tag matching the input tag is found in the organisation, a new tag will be created.

seal.add_tag(tag)

Adds a tag to the entity the script is embedded in. Can only be run from a script card.

Deleting tags

seal.delete_tag(tag_name)

Remove a tag by name from the entity within which the script is embedded.

seal.delete_tag_from_entity(entity_id, tag_name)

Remove a tag by name in any entity.

Getting workflow tasks

seal.get_workflow_tasks(entity_id, ?entity_version)

Gets all workflow tasks associated with a workflow entity. Returns a list of dictionaries, each representing a full workflow task object.

seal.get_workflow_tasks_for_containing_entity()

Gets all workflow tasks for the entity that a script is embedded in. Can only be run from a script card.

Getting workflows that reference an entity

seal.get_workflows_referencing(entity_id)

Gets all workflow entities that have tasks referencing the specified entity. Returns an array of full workflow entities.

seal.get_containing_entity_workflows_referencing()

Gets all workflow entities that have tasks referencing the entity the script is embedded in. Can only be run from a script card.

Getting change sets

seal.get_change_set_for_entity(entity_id, ?version)
seal.get_change_set_for_entity(ref={"id": "...", "version": "..."})

Returns information about the change set the given entity belongs to. This information includes id, index, name, status and description. By default, we find the change set for the latest draft of the entity (if it exists).

To find the change set that a specific version of an entity belongs to, include the version as a second argument. You can also pass an entity ref object instead.

seal.get_change_set_for_containing_entity()

Gets the change set for the entity that a script is embedded in. Can only be run from a script card.

Adding fields

seal.add_field(field_name, field_type, value, allow_multiple, select_options, multi_line, formula_expression)

Add a new field to the embedded-in entity.

For specific field types, refer to this specific section in the Scripts docs.

seal.add_field_to_entity(entity_id, field_name, field_type, value, allow_multiple, select_options, multi_line, formula_expression)

Add a new field to any entity.

Updating fields

seal.update_field_value(field_name, field_value)

Update a field value in the entity the script is embedded in. Can only be run from a script card.

seal.update_field_value_in_entity(entity_id, field_name, field_value)

Update a field value in any entity.

Deleting fields

seal.delete_field(field_name)

Remove a field from the embedded-in entity. Note that the field's data will also be removed.

seal.delete_field_in_entity(entity_id, field_name)

Delete field in any entity.

Updating properties

seal.update_entity_title(entity_id, title, ?overwrite_computed_title)

Update the title of any entity. If overwrite_computed_title is set to True and the instance has a computed title, this will be overwritten.

seal.update_containing_entity_title(title, ?overwrite_computed_title)

Update the title of the entity the script is embedded in. Can only be run from a script card.

Getting out of spec fields

seal.get_out_of_spec_fields(entity_id, ?version)
seal.get_out_of_spec_fields(ref={"id: "...", "version": "..."})

Gets the out of spec fields for the provided entity. By default, the latest version or draft of the entity is used.

To view the out of spec fields from a specific version of an entity, include the version as a second argument. You can also pass an entity ref object instead.

Handling files

seal.download_file(file_entity_id, ?version)

The file is downloaded to the local filesystem of the virtual machine instance. The value returned is the local file path as a string.

seal.upload_file(file_path, file_name, type_title, ?extract_fields_with_ai)

Uploads a file into a new entity. The provided type must have 'File' content type. Returns the entire json blob of data of the new entity.

AI data extraction

seal.extract_data_from_file(file_entity_id, version, ?=prompt)

Extract data from a file entity. See Parameters below:

  • file_entity_id: ID of the file entity to extract data from

  • version: (Optional) Specific version of the file entity

  • prompt: (Optional, keyword-only) Custom prompt to guide the data extraction

Returns: json Example:

data = seal.extract_data_from_file(
    "9351e027-d26c-4769-a395-507f4f148cb4",
     prompt="Extract all temperature readings by day"
)

Submitting Data

seal.submit_to_instance_submission_field(
   "field_name",
   field_values_df=pd.DataFrame([{ "field": "value", "Title": 'title'}]),
   number_of_empty_instances=3
   template_ref={"id": "...", "version": "..."}
)

Submit instances to a submission field in the surrounding entity.

Submit one or more instances to an instance submission field, with optional initial field values in a dataframe. If a column is called 'title' or 'Title', it will set the instances' titles of setting the values of a field called 'title' or 'Title').

You can optionally specify a number of empty instances to create.

If the submission field refers to a type, a template_ref to a template of the type must be provided.

You can also submit data to a submission table in any entity:

seal.submit_to_instance_submission_field_in_entity(entity_id, ...the same)

Submission Placeholder Columns

Placeholder columns are additional fields that can be added to all rows in a submission table. These columns are automatically added to existing instances in the submission field.

seal.add_placeholder_column_to_submission_field(field_name, column_name, column_type)

Add a placeholder column to an instance submission field in the entity the script is embedded in. Can only be run from a script card.

seal.add_placeholder_column_to_submission_field_in_entity(entity_id, field_name, column_name, column_type)

Add a placeholder column to an instance submission field in any entity.

seal.update_placeholder_column_config(field_name, column_name, *, allow_multiple=None, select_options=None, multi_line=None, format=None)

Update the configuration of a placeholder column in the entity the script is embedded in. Configuration parameters are keyword-only and optional. Can only be run from a script card.

seal.update_placeholder_column_config_in_entity(entity_id, field_name, column_name, *, allow_multiple=None, select_options=None, multi_line=None, format=None)

Update the configuration of a placeholder column in any entity. Configuration parameters are keyword-only and optional.

seal.remove_placeholder_column_from_submission_field(field_name, column_name)

Remove a placeholder column from an instance submission field in the entity the script is embedded in. Can only be run from a script card.

seal.remove_placeholder_column_from_submission_field_in_entity(entity_id, field_name, column_name)

Remove a placeholder column from an instance submission field in any entity.

Creating Instances

seal.create_instance_from_template(
  template_id,
  ?version,
  field_values={ "field": "value", ... },
  title='title'
)
seal.create_instance_from_template(
  ref={"id: "...", "version": "..."},
  field_values={ "field": "value", ... },
  title='title'
)

Create an instance from a template. By default, instances are created from the latest published version of the template.

To create an instance from a specific version of a template, include the version as a second argument. You can also pass an entity ref object instead.

Parameters:

  • template_id: ID of the template to create an instance from

  • version: (Optional, keyword-only) Specific version of the template

  • field_values: (Optional, keyword-only) Initial field values for the instance. The specified fields must exist on the template.

Similarly, you can also create test instances from a template:

seal.create_test_instance_from_template(
  template_id,
  version,
  field_values={ "field": "value", ... },
)

Creating Charts

chart = alt.Chart(data).mark_bar().encode(x="x", y="y")
chart_id = seal.create_chart(chart, title="Sample Chart", type_title="Chart")

Generate a chart from an Altair chart. See parameters below:\

  • chart: The Altair chart instance

  • title:The name for the chart

  • type_title: The name of the type to create a chart from. The content type must be Chart.

Running Scripts

seal.run_script(
  script_id,
  ?version
)
seal.run_script(
  ref={"id: "...", "version": "..."},
)

Run an entity that has content type Script code. By default, the latest version of the entity is run.

To run a specific version of a script entity, include the version as a second argument. You can also pass an entity ref object instead.

Parameters:

  • script_id: ID of the script to run

  • version: (Optional, keyword-only) Specific version of the script

seal.run_embedded_scripts(
  entity_id,
  ?version,
  ?card_ids=["..."]
)
seal.run_embedded_scripts(
  ref={"id: "...", "version": "..."},
  ?card_ids=["..."]
)

Parameters:

  • entity_id: ID of the entity with embedded scripts

  • version: (Optional, keyword-only) Specific version of the entity

  • card_ids: (Optional, keyword-only) List of specific card ids to run on the page

Run all action buttons and script cards embedded in an entity with page content. You can optionally pass in the card_ids argument to specify which scripts in particular should be run. These IDs can be found in the page content of the entity (see the Entity Schema for more details).

seal.get_live_backlinks(entity_id)

Returns the ids of all entities whose live data includes a reference to any version of the requested entity.

Getting trigger info

seal.get_trigger_info()

If the script is being run as a trigger, this method returns an object containing context about the trigger run: trigger_id and triggered_by_entity_id.

Importing packages

Seal comes with many common Python packages pre-installed. If there are other python packages you regularly require, please contact Seal support.

These packages are automatically imported in every Script, so don't need to be manually imported:

  • altair (also aliased as alt)

  • Chart (alias of altair.Chart)

  • pandas (also aliased as pd)

Available packages to import include:
  • altair

  • annotated-types

  • attrs

  • blinker

  • cachetools

  • certifi

  • cffi

  • charset-normalizer

  • click

  • cloudevents

  • contourpy

  • cryptography

  • cycler

  • deprecation

  • et-xmlfile

  • flask

  • fonttools

  • functions-framework

  • gcloud

  • google-api-core

  • google-auth

  • google-cloud-appengine-logging

  • google-cloud-audit-log

  • google-cloud-core

  • google-cloud-error-reporting

  • google-cloud-logging

  • google-cloud-storage

  • google-crc32c

  • google-resumable-media

  • googleapis-common-protos

  • grpc-google-iam-v1

  • grpcio

  • grpcio-status

  • gunicorn

  • httplib2

  • idna

  • itsdangerous

  • jinja2

  • joblib

  • jsonschema

  • jsonschema-specifications

  • jwcrypto

  • kiwisolver

  • lxml

  • markupsafe

  • matplotlib

  • numpy

  • oauth2client

  • openpyxl

  • packaging

  • pandas

  • pillow

  • proto-plus

  • protobuf

  • pyasn1

  • pyasn1-modules

  • pycorn

  • pycparser

  • pycryptodome

  • pydantic

  • pydantic-core

  • pyparsing

  • pyrebase4

  • python-dateutil

  • python-docx

  • python-jwt

  • pytz

  • referencing

  • requests

  • requests-toolbelt

  • rpds-py

  • rsa

  • scikit-learn

  • scipy

  • setuptools

  • six

  • tabulate

  • threadpoolctl

  • toolz

  • typing-extensions

  • urllib3

  • vl-convert-python

  • watchdog

  • werkzeug

  • xlrd

To temporarily install a package when running the script:

import subprocess
subprocess.check_output("pip install {name of package}".split())

Examples

Creating and Linking Charts

This example script would be embedded in an entity containing a SUBMISSION field, with your chart data, and a REFERENCE field to embed the generated chart in.

import pandas as pd
import altair as alt

# Name of the chart entity to create
CHART_TITLE = "Growth Rate"
# Your type entity with content type: 'Chart'
# Find or create the type from org setting -> types
CHART_TYPE_NAME = "Chart"
# The containing entity REFERENCE field to append the generated file entity to
REFERENCE_FIELD_NAME = "Charts"
# Submission field name containing your chart data
SUBMISSION_FIELD_NAME = "Data"
# Submission column name to plot on the X axis
SUBMISSION_X_COLUMN = "Time"
# Submission column name to plot on the Y axis
SUBMISSION_Y_COLUMN = "Growth"

# Extract data from submission field
entity = seal.get_containing_entity()
submission_field = entity["fields"].get(SUBMISSION_FIELD_NAME, {})
submitted_refs = submission_field.get("value", [])

# Collect data from submitted entities
data = []
for ref in submitted_refs:
    submitted_entity = seal.get_entity(ref=ref)
    fields = submitted_entity["fields"]
    
    timepoint = fields.get(SUBMISSION_X_COLUMN, {}).get("value")
    growth_rate = fields.get(SUBMISSION_Y_COLUMN, {}).get("value")
    
    if timepoint and growth_rate:
        data.append({SUBMISSION_X_COLUMN: timepoint, SUBMISSION_Y_COLUMN: growth_rate})

# Create DataFrame and chart
df = pd.DataFrame(data)
chart = alt.Chart(df).mark_line(point=True).encode(x=SUBMISSION_X_COLUMN, y=SUBMISSION_Y_COLUMN)

# Create chart entity and link to reference field
chart_id = seal.create_chart(chart, title=CHART_TITLE, type_title=CHART_TYPE)

existing_refs = entity["fields"].get(REFERENCE_FIELD_NAME, {}).get("value", []) or []
updated_refs = existing_refs + [{"id": chart_id, "version": None}]
seal.update_field_value(REFERENCE_FIELD_NAME, updated_refs)

Creating Labels

Create PDF labels from entity data with configurable layouts. This method requires the Label creation blueprint to be installed. To setup this up, see Creating labels blueprint.

seal.create_label_v1()

This script uses the ReportLab PDF Library to create labels with text, barcodes (Code128/Code39/QR), images, and geometric elements. It automatically operates in Preview Mode when embedded in a configuration entity (for testing) or Trigger Mode when triggered by entity updates (for automatic label generation). The configuration is stored in entities with layout elements defined in submission tables.

Complete Label Generation Script
import io
import os
import time

from reportlab.graphics.barcode import code39, code128, qr  # type: ignore
from reportlab.lib.colors import black, blue, gray, red, white  # type: ignore
from reportlab.lib.units import cm, inch, mm  # type: ignore
from reportlab.pdfbase.pdfmetrics import stringWidth  # type: ignore
from reportlab.pdfgen import canvas  # type: ignore

print("=== LABEL GENERATION SCRIPT STARTING ===")

UNIT_OBJECTS = {"cm": cm, "mm": mm, "inch": inch}

COLOR_MAP = {
    "Black": black,
    "Gray": gray,
    "Blue": blue,
    "Red": red,
    "White": white,
    "None": None,
}


def extract_field_value(field_dict, field_name, default_value):
    """Extract field value handling SELECT arrays and whitespace"""
    field_value = field_dict.get(field_name, {}).get("value", default_value)

    if isinstance(field_value, list) and field_value:
        result = field_value[0]
    elif isinstance(field_value, list):
        result = default_value
    else:
        result = field_value if field_value is not None else default_value

    if isinstance(result, str):
        result = result.strip()

    return result


def find_config_for_template(config_list_refs, target_template_id):
    for config_row_ref in config_list_refs:
        try:
            config_row = seal.get_entity(entity_id=config_row_ref["id"])
            config_row_fields = config_row.get("fields", {})

            # Get the Trigger Template reference from this row
            trigger_template_refs = config_row_fields.get("Trigger Template", {}).get(
                "value", []
            )

            if trigger_template_refs and isinstance(trigger_template_refs, list):
                trigger_template_id = trigger_template_refs[0].get("id", "")

                if trigger_template_id == target_template_id:
                    # Found matching template, get the Label Configuration
                    label_config_refs = config_row_fields.get(
                        "Label Configuration", {}
                    ).get("value", [])

                    if (
                        label_config_refs
                        and isinstance(label_config_refs, list)
                        and len(label_config_refs) > 0
                    ):
                        return label_config_refs[0].get("id")
                    else:
                        logger.warning(
                            "Configuration row has matching template but no Label Configuration"
                        )

        except Exception as e:
            logger.warning(
                "Failed to process configuration row %s: %s",
                config_row_ref["id"],
                str(e),
            )
            continue

    return None


def main():
    """Main entry point with dual-mode detection"""
    try:
        print("Loading script configuration...")
        config = load_script_configuration()

        try:
            trigger_info = seal.get_trigger_info()
            logger.info("Running in PRODUCTION MODE")
            run_production_mode(config, trigger_info.triggered_by_entity_id)
        except Exception as e:
            if "not called as a trigger" in str(e):
                logger.info("Running in PREVIEW MODE")
                run_preview_mode(config)
            else:
                raise e

    except Exception as e:
        logger.error("Script execution failed: %s", str(e), exc_info=True)
        raise


def load_script_configuration():
    """Load configuration from referenced configuration entity"""
    script_entity_id = seal.entity_id

    script_entity = seal.get_entity(entity_id=script_entity_id)
    script_fields = script_entity.get("fields", {})

    # Determine which configuration entity to use
    config_entity = None
    config_field_name = None

    try:
        # Try to get trigger info to determine the triggering entity template
        trigger_info = seal.get_trigger_info()
        triggering_entity_id = trigger_info.triggered_by_entity_id
        triggering_entity = seal.get_entity(entity_id=triggering_entity_id)
        triggering_template_ref = triggering_entity.get("template", {})
        triggering_template_id = triggering_template_ref.get("id", "")

        if not triggering_template_id:
            raise Exception("Triggering entity has no template ID")

        # Get the Configuration List submission field
        config_list_field = script_fields.get("Configuration List", {})
        config_list_refs = config_list_field.get("value", [])

        if not config_list_refs:
            raise Exception(
                "No Configuration List found in script. Please add a "
                "'Configuration List' submission field to the script entity."
            )

        # Search through configuration list rows for matching template
        config_entity_id = find_config_for_template(
            config_list_refs, triggering_template_id
        )

        if config_entity_id:
            config_field_name = f"Row for template {triggering_template_id}"
            logger.info(
                "Production mode: Found matching template '%s' in configuration list",
                triggering_template_id,
            )

        if not config_entity_id:
            raise Exception(
                f"No configuration found for template '{triggering_template_id}'. "
                f"Please add a row to the Configuration List with this template "
                f"in the 'Trigger Template' field and a configuration entity "
                f"in the 'Label Configuration' field."
            )

        config_entity = seal.get_entity(entity_id=config_entity_id)

    except Exception as e:
        # Any exception means we're in preview mode - script is embedded in the config entity
        logger.info("Falling back to preview mode: %s", str(e))

        try:
            config_entity = seal.get_containing_entity()
            config_field_name = "Preview Mode"  # Just for logging purposes
            logger.info("Preview mode: Using containing entity as configuration")

            # Validate that the script has a properly configured Configuration List
            preview_entities_refs = (
                config_entity.get("fields", {})
                .get("Preview Entities", {})
                .get("value", [])
            )

            if preview_entities_refs:
                # Check if script entity has Configuration List field
                config_list_field = script_fields.get("Configuration List", {})
                config_list_refs = config_list_field.get("value", [])

                if not config_list_refs:
                    raise Exception(
                        "Script validation failed: No 'Configuration List' submission "
                        "field found in script entity. Please add a 'Configuration List' "
                        "submission field to the script."
                    )

                # Check that preview entities' templates are covered in Configuration List
                first_preview_entity = seal.get_entity(
                    entity_id=preview_entities_refs[0]["id"]
                )
                preview_template_ref = first_preview_entity.get("template", {})
                preview_template_id = preview_template_ref.get("id", "")

                if preview_template_id:
                    # Check if Configuration List contains this template and points to current config
                    found_config_id = find_config_for_template(
                        config_list_refs, preview_template_id
                    )

                    if not found_config_id:
                        raise Exception(
                            f"Script validation failed: No row in Configuration List "
                            f"matches preview entity template '{preview_template_id}'. "
                            f"Please add a row with this template in 'Trigger Template' field."
                        )

                    if found_config_id != config_entity["id"]:
                        raise Exception(
                            f"Script validation failed: Configuration List contains "
                            f"template '{preview_template_id}' but points to config "
                            f"'{found_config_id}' instead of this configuration entity "
                            f"'{config_entity['id']}'. Please update the 'Label Configuration' "
                            f"field in the matching row."
                        )

                    config_field_name = "Preview Mode (validated Configuration List)"
                    logger.info(
                        "Preview mode validation passed: Script Configuration List "
                        "correctly configured for template '%s'",
                        preview_template_id,
                    )
                else:
                    logger.warning(
                        "Preview entities found but first entity has no template - "
                        "skipping script validation"
                    )
            else:
                logger.warning(
                    "No preview entities configured - skipping script validation"
                )

        except Exception as preview_error:
            raise Exception(
                f"Cannot determine configuration entity. Production mode failed: "
                f"{str(e)}. Preview mode failed: {str(preview_error)}"
            )

    if not config_entity:
        raise Exception("Could not load configuration entity")

    logger.info(
        "Using configuration: %s (ID: %s)", config_field_name, config_entity["id"]
    )

    fields = config_entity.get("fields", {})

    units = extract_field_value(fields, "Units", "cm")

    label_width = extract_field_value(fields, "Label Width", 10.0)
    label_height = extract_field_value(fields, "Label Height", 5.0)
    logger.info("Label configuration: %s x %s %s", label_width, label_height, units)

    config = {
        "Label Width": label_width,
        "Label Height": label_height,
        "Default Font": extract_field_value(fields, "Default Font", "Helvetica"),
        "Default Font Size": extract_field_value(fields, "Default Font Size", 10),
        "Label Title Field Name": extract_field_value(
            fields, "Label Title Field Name", "title"
        ),
        "Units": units,
        "Preview Entities": fields.get("Preview Entities", {}).get("value", []),
    }

    # Load layout elements from submission table
    layout_elements_field = fields.get("Label Elements", {})
    layout_elements = []

    if layout_elements_field and "value" in layout_elements_field:
        element_refs = layout_elements_field["value"]

        if not element_refs:
            logger.warning(
                "Label Elements field is empty - no elements will be rendered"
            )

        for element_ref in element_refs:
            try:
                element_entity = seal.get_entity(entity_id=element_ref["id"])
                element_fields = element_entity.get("fields", {})

                # Extract element configuration with defaults
                element = {
                    "Element Type": extract_field_value(
                        element_fields, "Element Type", "Text"
                    ),
                    "Text": extract_field_value(
                        element_fields, "Text", ""
                    ),  # For Text elements
                    "Field Name": extract_field_value(
                        element_fields, "Field Name", ""
                    ),  # For all other elements
                    "Prefix Text": extract_field_value(
                        element_fields, "Prefix Text", ""
                    ),
                    "X Position": extract_field_value(element_fields, "X Position", 0),
                    "Y Position": extract_field_value(element_fields, "Y Position", 0),
                    "Width": extract_field_value(element_fields, "Width", 2.0),
                    "Height": extract_field_value(element_fields, "Height", 1.0),
                    "Font Name": extract_field_value(
                        element_fields, "Font Name", "Helvetica"
                    ),
                    "Font Size": extract_field_value(
                        element_fields, "Font Size", None
                    ),  # None = use default
                    "Text Alignment": extract_field_value(
                        element_fields, "Text Alignment", "Left"
                    ),
                    "Alignment": extract_field_value(
                        element_fields, "Alignment", "Top-Left"
                    ),  # Universal alignment
                    "Rotation (°)": extract_field_value(
                        element_fields, "Rotation (°)", 0
                    ),  # Rotation in degrees
                    "Line Width": extract_field_value(element_fields, "Line Width", 1),
                    "Stroke Color": extract_field_value(
                        element_fields, "Stroke Color", "Black"
                    ),
                }
                layout_elements.append(element)

            except Exception as e:
                logger.warning(
                    "Failed to load layout element %s: %s", element_ref["id"], str(e)
                )
                continue

    config["Label Elements"] = layout_elements
    config["Configuration Entity ID"] = config_entity["id"]
    logger.info(
        "Loaded %d label elements using config '%s'",
        len(layout_elements),
        config_field_name,
    )
    return config


def convert_coordinates(x, y, canvas_obj):
    """
    Convert from top-left coordinate system to ReportLab's bottom-left system

    User coordinates: (0,0) = top-left, Y increases downward
    ReportLab: (0,0) = bottom-left, Y increases upward
    """
    height = canvas_obj._label_height
    return x, height - y


def calculate_aligned_position(x, y, width, height, alignment):
    """
    Calculate actual drawing position based on alignment anchor point

    Args:
        x, y: User-specified coordinates
        width, height: Element dimensions (in same units as x, y)
        alignment: "Top-Left", "Center", "Top-Right", "Bottom-Left", "Bottom-Right"

    Returns:
        (adjusted_x, adjusted_y): Actual position for drawing
    """
    if alignment == "Center":
        return x - width / 2, y - height / 2
    elif alignment == "Top-Right":
        return x - width, y
    elif alignment == "Bottom-Left":
        return x, y - height
    elif alignment == "Bottom-Right":
        return x - width, y - height
    else:  # "Top-Left" or any other value defaults to top-left
        return x, y


def calculate_drawing_position_from_anchor(
    anchor_x, anchor_y, actual_width, actual_height, alignment
):
    """
    Calculate where to draw based on actual dimensions and fixed anchor point

    This function ensures consistent alignment by treating user coordinates as immutable
    anchor points, regardless of actual rendered dimensions.

    Args:
        anchor_x, anchor_y: User-specified coordinates (the anchor point)
        actual_width, actual_height: Real rendered dimensions
        alignment: How the anchor relates to the element

    Returns:
        (draw_x, draw_y): Where to start drawing the element
    """
    if alignment == "Center":
        return anchor_x - actual_width / 2, anchor_y - actual_height / 2
    elif alignment == "Top-Right":
        return anchor_x - actual_width, anchor_y
    elif alignment == "Bottom-Left":
        return anchor_x, anchor_y - actual_height
    elif alignment == "Bottom-Right":
        return anchor_x - actual_width, anchor_y - actual_height
    else:  # "Top-Left" or any other value defaults to top-left
        return anchor_x, anchor_y


def apply_rotation_to_canvas(canvas_obj, rotation_degrees, anchor_x, anchor_y):
    """
    Apply rotation around a specific anchor point

    Args:
        canvas_obj: ReportLab canvas object
        rotation_degrees: Rotation angle in degrees (clockwise)
        anchor_x, anchor_y: Point to rotate around (in ReportLab coordinates)
    """
    if rotation_degrees != 0:
        # Save current state
        canvas_obj.saveState()

        # Translate to anchor point, rotate, then translate back
        canvas_obj.translate(anchor_x, anchor_y)
        canvas_obj.rotate(rotation_degrees)
        canvas_obj.translate(-anchor_x, -anchor_y)

        return True  # Indicates rotation was applied
    return False  # No rotation applied


def render_element_with_rotation(canvas_obj, element, x, y, unit_obj, render_callback):
    """
    Base render function that handles common rotation setup/teardown for all element types

    Args:
        canvas_obj: ReportLab canvas object
        element: Element configuration dict
        x, y: User coordinates
        unit_obj: ReportLab unit object (cm, mm, inch)
        render_callback: Function that performs the actual rendering
                        Signature: render_callback(canvas_obj, element, x, y, unit_obj, anchor_coords)
                        where anchor_coords is (anchor_x_rl, anchor_y_rl)
    """
    rotation = element.get("Rotation (°)", 0)
    if isinstance(rotation, list) and rotation:
        rotation = rotation[0]
    if not isinstance(rotation, (int, float)):
        rotation = 0

    anchor_x = x * unit_obj
    anchor_y = y * unit_obj
    anchor_x_rl, anchor_y_rl = convert_coordinates(anchor_x, anchor_y, canvas_obj)

    rotation_applied = apply_rotation_to_canvas(
        canvas_obj, rotation, anchor_x_rl, anchor_y_rl
    )

    try:
        render_callback(canvas_obj, element, x, y, unit_obj, (anchor_x_rl, anchor_y_rl))

    finally:
        if rotation_applied:
            canvas_obj.restoreState()


def check_page_break(y_position, canvas_obj, min_space=50):
    """
    Check if we need a page break and create one if necessary
    Returns the new Y position (reset to top if page break occurred)
    """
    if y_position < min_space:  # Less than min_space points from bottom
        canvas_obj.showPage()
        # After page break, we're back to top-left coordinates
        return canvas_obj._label_height - 20  # Start 20 points from top
    return y_position


def create_label_pdf(config, entity_data):
    """
    SHARED label generation core used by both modes

    Args:
        config: Dict of configuration values (width, height, label elements, etc.)
        entity_data: Dict of entity field values

    Returns:
        PDF buffer ready for upload
    """
    logger.info("Starting PDF generation...")
    units = config.get("Units", "cm")
    unit_obj = UNIT_OBJECTS[units]

    width = config.get("Label Width", 10.0) * unit_obj
    height = config.get("Label Height", 5.0) * unit_obj

    pdf_buffer = io.BytesIO()
    canvas_obj = canvas.Canvas(pdf_buffer, pagesize=(width, height))

    # Store dimensions for coordinate conversion
    canvas_obj._label_width = width
    canvas_obj._label_height = height

    layout_elements = config.get("Label Elements", [])
    logger.info("Processing %d label elements", len(layout_elements))

    for i, element in enumerate(layout_elements):
        try:
            render_layout_element(canvas_obj, element, entity_data, unit_obj, config)
        except Exception as e:
            logger.error(
                "Failed to render layout element %d: %s", i + 1, str(e), exc_info=True
            )
            continue

    canvas_obj.save()
    pdf_buffer.seek(0)
    return pdf_buffer


def render_layout_element(canvas_obj, element, entity_data, unit_obj, config):
    """Render a single layout element on the canvas"""
    element_type = element.get("Element Type", "").strip()

    if element_type == "Text":
        value_or_field_name = element.get("Text", "").strip()
    else:
        value_or_field_name = element.get("Field Name", "").strip()

    x_pos = element.get("X Position", 0)
    y_pos = element.get("Y Position", 0)

    alignment = element.get("Alignment", "Top-Left")
    if isinstance(alignment, list) and alignment:
        alignment = alignment[0].strip()
    elif isinstance(alignment, str):
        alignment = alignment.strip()
    else:
        alignment = "Top-Left"
    x = x_pos * unit_obj
    y = y_pos * unit_obj

    # Convert from top-left to bottom-left coordinates
    x_final, y_final = convert_coordinates(x, y, canvas_obj)

    if element_type == "Page Break":
        canvas_obj.showPage()
        return

    prefix_text = element.get("Prefix Text", "").strip()

    if element_type == "Text":
        base_value = value_or_field_name
    elif element_type == "Field":
        base_value = entity_data.get(value_or_field_name, "")
        if not base_value and value_or_field_name:
            logger.warning(
                "Field '%s' not found in entity data, skipping element",
                value_or_field_name,
            )
            return
    elif element_type in ["Barcode Code128", "QR Code", "Barcode Code39"]:
        base_value = entity_data.get(value_or_field_name, "")
        if not base_value and value_or_field_name:
            logger.warning(
                "Field '%s' not found in entity data, skipping element",
                value_or_field_name,
            )
            return
        # For barcodes, use base value without prefix (barcodes need clean data)
        final_value = base_value
    elif element_type == "Image":
        image_refs = entity_data.get(value_or_field_name, "")
        if not image_refs:
            logger.warning(
                "Image reference field '%s' not found, skipping element",
                value_or_field_name,
            )
            return
        final_value = image_refs
    elif element_type == "Border":
        base_value = ""
        final_value = ""
    else:
        logger.warning("Unknown element type: %s", element_type)
        return

    if element_type in ["Text", "Field"]:
        final_value = f"{prefix_text}{base_value}" if prefix_text else base_value
    # Render the appropriate element
    if element_type in ["Text", "Field"]:
        render_text_element(
            canvas_obj, element, final_value, x_pos, y_pos, unit_obj, config, alignment
        )
    elif element_type in ["Barcode Code128", "QR Code", "Barcode Code39"]:
        render_barcode_element(
            canvas_obj, element, final_value, x_pos, y_pos, unit_obj, alignment
        )
    elif element_type == "Image":
        render_image_element(
            canvas_obj, element, final_value, x_pos, y_pos, unit_obj, alignment
        )
    elif element_type == "Border":
        render_rectangle_element(canvas_obj, element, x_pos, y_pos, unit_obj, alignment)


def render_text_element(
    canvas_obj, element, field_value, x, y, unit_obj, config, alignment
):
    """Render text element with universal alignment and rotation support"""

    def render_text_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific text rendering logic"""
        font_name = element.get("Font Name", "Helvetica")
        font_size = element.get("Font Size", None)
        if font_size is None:
            font_size = config.get("Default Font Size", 10)
        stroke_color = COLOR_MAP.get(element.get("Stroke Color", "Black"), black)

        text_alignment = element.get("Text Alignment", "Left")
        if isinstance(text_alignment, list) and text_alignment:
            text_alignment = text_alignment[0].strip()
        elif isinstance(text_alignment, str):
            text_alignment = text_alignment.strip()
        else:
            text_alignment = "Left"

        text_str = str(field_value)

        # Use ReportLab's stringWidth for accurate text width measurement
        text_width = stringWidth(text_str, font_name, font_size)
        text_height = font_size

        draw_x, draw_y = calculate_drawing_position_from_anchor(
            x, y, text_width / unit_obj, text_height / unit_obj, alignment
        )

        draw_x_final = draw_x * unit_obj
        draw_y_final = draw_y * unit_obj
        draw_x_rl, draw_y_rl = convert_coordinates(
            draw_x_final, draw_y_final, canvas_obj
        )

        canvas_obj.setFont(font_name, font_size)
        if stroke_color:
            canvas_obj.setFillColor(stroke_color)

        # Adjust Y coordinate: ReportLab draws text from baseline, we want top-left positioning
        text_y = (
            draw_y_rl - font_size * 0.8
        )  # 0.8 factor accounts for typical ascender height

        if text_alignment == "Center":
            canvas_obj.drawCentredText(draw_x_rl, text_y, text_str)
        elif text_alignment == "Right":
            canvas_obj.drawRightString(draw_x_rl, text_y, text_str)
        else:
            canvas_obj.drawString(draw_x_rl, text_y, text_str)

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_text_callback
    )


def render_barcode_element(canvas_obj, element, field_value, x, y, unit_obj, alignment):
    """Render barcode element with universal alignment and rotation support"""

    def render_barcode_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific barcode rendering logic"""
        element_type = element.get("Element Type")
        width = element.get("Width", 2.0) * unit_obj
        height = element.get("Height", 1.0) * unit_obj

        try:
            canvas_obj.saveState()

            canvas_obj.setStrokeColor(black)
            canvas_obj.setFillColor(black)

            if element_type == "Barcode Code128":
                barcode = code128.Code128(
                    value=str(field_value),
                    barHeight=height,
                    barWidth=width / 100,
                    humanReadable=True,
                    quiet=0,  # Disable quiet zones for precise alignment
                    lquiet=0,
                    rquiet=0,
                )

                actual_barcode_width = barcode.width

                draw_x, draw_y = calculate_drawing_position_from_anchor(
                    x, y, actual_barcode_width / unit_obj, height / unit_obj, alignment
                )
                draw_x_final = draw_x * unit_obj
                draw_y_final = draw_y * unit_obj
                draw_x_rl, draw_y_rl = convert_coordinates(
                    draw_x_final, draw_y_final, canvas_obj
                )

                # Adjust Y coordinate: ReportLab draws barcodes from bottom-left, we want top-left positioning
                actual_barcode_y = draw_y_rl - height

                barcode.drawOn(canvas_obj, draw_x_rl, actual_barcode_y)

            elif element_type == "Barcode Code39":
                barcode = code39.Standard39(
                    value=str(field_value),
                    barHeight=height,
                    barWidth=width / 100,
                    humanReadable=True,
                    quiet=0,  # Disable quiet zones for precise alignment
                    lquiet=0,
                    rquiet=0,
                )

                actual_barcode_width = barcode.width

                draw_x, draw_y = calculate_drawing_position_from_anchor(
                    x, y, actual_barcode_width / unit_obj, height / unit_obj, alignment
                )
                draw_x_final = draw_x * unit_obj
                draw_y_final = draw_y * unit_obj
                draw_x_rl, draw_y_rl = convert_coordinates(
                    draw_x_final, draw_y_final, canvas_obj
                )

                # Adjust Y coordinate: ReportLab draws barcodes from bottom-left, we want top-left positioning
                actual_barcode_y = draw_y_rl - height

                barcode.drawOn(canvas_obj, draw_x_rl, actual_barcode_y)

            elif element_type == "QR Code":
                draw_x, draw_y = calculate_drawing_position_from_anchor(
                    x, y, width / unit_obj, height / unit_obj, alignment
                )
                draw_x_final = draw_x * unit_obj
                draw_y_final = draw_y * unit_obj
                draw_x_rl, draw_y_rl = convert_coordinates(
                    draw_x_final, draw_y_final, canvas_obj
                )

                qr_code = qr.QrCodeWidget(str(field_value))
                qr_code.barWidth = width
                qr_code.barHeight = height

                from reportlab.graphics import renderPDF  # type: ignore
                from reportlab.graphics.shapes import Drawing  # type: ignore

                drawing = Drawing(width, height)
                drawing.add(qr_code)

                # Adjust Y coordinate: ReportLab draws from bottom-left, we want top-left positioning
                qr_y = draw_y_rl - height
                renderPDF.draw(drawing, canvas_obj, draw_x_rl, qr_y)

            canvas_obj.restoreState()

        except Exception as e:
            logger.error("Failed to render barcode %s: %s", element_type, str(e))
            canvas_obj.restoreState()

            fallback_x, fallback_y = calculate_drawing_position_from_anchor(
                x, y, 100 / unit_obj, 8 / unit_obj, alignment
            )
            fallback_x_final = fallback_x * unit_obj
            fallback_y_final = fallback_y * unit_obj
            fallback_x_rl, fallback_y_rl = convert_coordinates(
                fallback_x_final, fallback_y_final, canvas_obj
            )

            canvas_obj.setFont("Helvetica", 8)
            fallback_text_y = fallback_y_rl - 8 * 0.8
            canvas_obj.drawString(
                fallback_x_rl, fallback_text_y, f"Barcode: {field_value}"
            )

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_barcode_callback
    )


def render_rectangle_element(canvas_obj, element, x, y, unit_obj, alignment):
    """Render border element with universal alignment and rotation support"""

    def render_rectangle_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific rectangle/border rendering logic"""
        width = element.get("Width", 2.0) * unit_obj
        height = element.get("Height", 1.0) * unit_obj
        line_width = element.get("Line Width", 1)
        stroke_color = COLOR_MAP.get(element.get("Stroke Color", "Black"))

        aligned_x, aligned_y = calculate_aligned_position(
            x, y, width / unit_obj, height / unit_obj, alignment
        )

        x_final = aligned_x * unit_obj
        y_final = aligned_y * unit_obj
        x_rl, y_rl = convert_coordinates(x_final, y_final, canvas_obj)

        canvas_obj.setLineWidth(line_width)
        if stroke_color:
            canvas_obj.setStrokeColor(stroke_color)

        # Adjust Y coordinate: ReportLab draws rectangles from bottom-left, we want top-left positioning
        rect_y = y_rl - height

        stroke_flag = 1 if stroke_color else 0
        canvas_obj.rect(x_rl, rect_y, width, height, stroke=stroke_flag, fill=0)

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_rectangle_callback
    )


def render_image_element(canvas_obj, element, image_refs, x, y, unit_obj, alignment):
    """Render image element with universal alignment and rotation support"""

    def render_image_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific image rendering logic"""
        width = element.get("Width", 2.0) * unit_obj
        height = element.get("Height", 1.0) * unit_obj

        aligned_x, aligned_y = calculate_aligned_position(
            x, y, width / unit_obj, height / unit_obj, alignment
        )

        x_final = aligned_x * unit_obj
        y_final = aligned_y * unit_obj
        x_rl, y_rl = convert_coordinates(x_final, y_final, canvas_obj)

        try:
            if isinstance(image_refs, list) and image_refs:
                image_ref = image_refs[0]
            elif isinstance(image_refs, dict):
                image_ref = image_refs
            else:
                logger.warning("Invalid image reference format: %s", image_refs)
                return

            image_entity_id = image_ref.get("id", "")
            if not image_entity_id:
                logger.warning("No image entity ID found in reference")
                return

            if image_ref.get("version"):
                temp_path = seal.download_file(
                    file_entity_id=image_entity_id, version=image_ref["version"]
                )
            else:
                temp_path = seal.download_file(file_entity_id=image_entity_id)

            try:
                # Adjust Y coordinate: ReportLab draws images from bottom-left, we want top-left positioning
                image_y = y_rl - height

                canvas_obj.drawImage(
                    temp_path, x_rl, image_y, width=width, height=height, mask="auto"
                )

            finally:
                try:
                    os.unlink(temp_path)
                except Exception as cleanup_error:
                    logger.warning(
                        "Failed to clean up downloaded file: %s", cleanup_error
                    )

        except Exception as e:
            logger.error("Failed to render image: %s", str(e), exc_info=True)
            canvas_obj.setFont("Helvetica", 8)
            fallback_y = y_rl - 8 * 0.8
            canvas_obj.drawString(x_rl, fallback_y, "[Image Error]")

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_image_callback
    )


def extract_entity_fields(entity):
    """Extract all field values from an entity for label generation"""
    entity_data = {}
    fields = entity.get("fields", {})

    for field_name, field_info in fields.items():
        field_value = field_info.get("value", "")
        field_type = field_info.get("type", "")

        if isinstance(field_value, list):
            if (
                field_value
                and isinstance(field_value[0], dict)
                and "id" in field_value[0]
            ):
                if field_type == "REFERENCE":
                    entity_data[field_name] = field_value
                else:
                    entity_data[field_name] = ", ".join(
                        [ref.get("id", "") for ref in field_value]
                    )
            elif field_value:
                entity_data[field_name] = ", ".join([str(item) for item in field_value])
            else:
                entity_data[field_name] = ""
        elif isinstance(field_value, dict):
            entity_data[field_name] = str(field_value.get("value", field_value))
        else:
            entity_data[field_name] = (
                str(field_value) if field_value is not None else ""
            )

    return entity_data


def run_preview_mode(config):
    """Generate preview labels for testing"""
    preview_entities_refs = config.get("Preview Entities", [])

    if not preview_entities_refs:
        logger.info("No preview entities configured")
        return

    preview_labels = []

    for entity_ref in preview_entities_refs:
        try:
            logger.info("Generating preview for entity: %s", entity_ref["id"])
            entity = seal.get_entity(entity_id=entity_ref["id"])
            entity_data = extract_entity_fields(entity)
            pdf_buffer = create_label_pdf(config, entity_data)
            label_file = upload_pdf_as_preview(pdf_buffer, entity, config)
            preview_labels.append(label_file)
            logger.info("Preview label created successfully: %s", label_file["id"])

        except Exception as e:
            logger.error(
                "Failed to generate preview for entity %s: %s",
                entity_ref["id"],
                str(e),
                exc_info=True,
            )
            continue

    update_preview_references(preview_labels, config)
    logger.info("Generated %d preview labels", len(preview_labels))


def run_production_mode(config, source_entity_id):
    """Generate label for triggering entity"""
    try:
        entity = seal.get_entity(entity_id=source_entity_id)
        entity_data = extract_entity_fields(entity)
        pdf_buffer = create_label_pdf(config, entity_data)
        create_or_update_production_label(pdf_buffer, entity, config)
        logger.info("Label generated for entity: %s", entity.get("title"))

    except Exception as e:
        logger.error("Failed to generate production label: %s", str(e), exc_info=True)
        raise


def upload_pdf_as_preview(pdf_buffer, source_entity, config):
    """Upload PDF as preview label file entity"""
    timestamp = str(int(time.time()))
    title_field_name = config.get("Label Title Field Name", "title")

    title_field_value = (
        source_entity.get("fields", {}).get(title_field_name, {}).get("value", None)
    )
    if isinstance(title_field_value, list):
        entity_title = str(title_field_value[0]) if title_field_value else "Preview"
    elif isinstance(title_field_value, dict):
        entity_title = str(title_field_value.get("value", "Preview"))
    elif title_field_value:
        entity_title = str(title_field_value)
    else:
        entity_title = source_entity.get("title", "Preview")

    filename = f"Preview_{entity_title}_{timestamp}.pdf"

    temp_filename = f"temp_preview_{timestamp}.pdf"
    with open(temp_filename, "wb") as f:
        f.write(pdf_buffer.read())

    try:
        file_entity = seal.upload_file(
            file_path=temp_filename, file_name=filename, type_title="Label"
        )

        return file_entity

    finally:
        if os.path.exists(temp_filename):
            os.remove(temp_filename)


def create_or_update_production_label(pdf_buffer, source_entity, config):
    """Create or update production label (adapted from fixedGenerationScript.py)"""
    source_entity_id = source_entity["id"]
    script_entity_id = seal.entity_id

    title_field_name = config.get("Label Title Field Name", "title")
    title_field_value = (
        source_entity.get("fields", {}).get(title_field_name, {}).get("value", None)
    )

    if isinstance(title_field_value, list):
        label_title = str(title_field_value[0]) if title_field_value else "Label"
    elif isinstance(title_field_value, dict):
        label_title = str(title_field_value.get("value", "Label"))
    elif title_field_value:
        label_title = str(title_field_value)
    else:
        label_title = source_entity.get("title", "Label")

    timestamp = str(int(time.time()))
    temp_filename = f"label_{source_entity_id}_{timestamp}.pdf"
    with open(temp_filename, "wb") as f:
        f.write(pdf_buffer.read())

    try:
        search_query = {
            "filters": {
                "fieldValue": [
                    {
                        "name": "Label Source",
                        "operator": "=",
                        "value": f"ref({source_entity_id})",
                    }
                ]
            }
        }

        existing_labels = seal.search_entities(search_query)

        # Manually filter by Label Generation Script to find the exact match
        existing_label_id = None
        for label in existing_labels:
            label_script_refs = (
                label.get("fields", {})
                .get("Label Generation Script", {})
                .get("value", [])
            )
            if any(ref.get("id") == script_entity_id for ref in label_script_refs):
                existing_label_id = label["id"]
                break

        if existing_label_id:
            logger.info("Updating existing label: %s", existing_label_id)
            update_existing_label(existing_label_id, temp_filename, label_title, config)
        else:
            logger.info("Creating new label for entity: %s", source_entity_id)
            create_new_label(
                temp_filename, label_title, source_entity_id, script_entity_id, config
            )

    finally:
        if os.path.exists(temp_filename):
            os.remove(temp_filename)


def update_existing_label(label_id, temp_filename, label_title, config):
    """Update existing label with new content"""
    existing_label = seal.get_entity(entity_id=label_id)
    if existing_label["status"] != "EDITABLE":
        seal.make_entity_editable(label_id)

    timestamp = str(int(time.time()))
    unique_filename = f"{label_title}_label_{timestamp}.pdf"

    temp_file_entity = seal.upload_file(
        file_path=temp_filename, file_name=unique_filename, type_title="Label"
    )

    new_file_id = temp_file_entity["content"]["value"]["fileId"]
    new_content = {"type": "FILE", "value": {"fileId": new_file_id}}

    seal._request(
        url=f"entities/{label_id}/content",
        method="PATCH",
        json={"content": new_content},
    )

    current_title = existing_label.get("title")
    if current_title != label_title:
        seal.update_entity_title(label_id, label_title)

    seal.archive_entity(temp_file_entity["id"], True)

    logger.info("Label updated successfully: %s", label_id)


def create_new_label(
    temp_filename, label_title, source_entity_id, script_entity_id, config
):
    """Create new label file entity"""
    # Upload the PDF file
    file_entity = seal.upload_file(
        file_path=temp_filename, file_name=f"{label_title}.pdf", type_title="Label"
    )

    # Update title
    seal.update_entity_title(file_entity["id"], label_title)

    # Set reference fields
    label_entity_id = file_entity["id"]

    # Label Source reference
    seal.update_field_value_in_entity(
        entity_id=label_entity_id,
        field_name="Label Source",
        field_value=[{"id": source_entity_id, "version": None}],
    )

    # Label Generation Script reference
    seal.update_field_value_in_entity(
        entity_id=label_entity_id,
        field_name="Label Generation Script",
        field_value=[{"id": script_entity_id, "version": None}],
    )

    logger.info("New label created successfully: %s", label_entity_id)


def update_preview_references(preview_labels, config):
    """Update the Preview Labels reference field in configuration entity"""
    if not preview_labels:
        return

    config_entity_id = config.get("Configuration Entity ID")
    if not config_entity_id:
        logger.warning("No configuration entity ID found, cannot update Preview Labels")
        return

    preview_refs = [{"id": label["id"], "version": None} for label in preview_labels]

    try:
        seal.update_field_value_in_entity(
            entity_id=config_entity_id,
            field_name="Preview Labels",
            field_value=preview_refs,
        )
        logger.info(
            "Updated Preview Labels on configuration entity with %d references",
            len(preview_refs),
        )
    except Exception as e:
        logger.error("Failed to update Preview Labels field: %s", str(e))


def create_label(seal_instance):
    global seal, logger

    seal = seal_instance

    import logging

    logger = logging.getLogger(__name__)

    return main()


# Only run main() when script is executed directly, not when imported
if __name__ == "__main__":
    main()

Last updated