Skip to content

import_design Script

scripts/import_design.py is the end-to-end command-line tool for ingesting a GIF animation into the Lighting Rig Backend. It orchestrates three stages:

Stage Description
1 – Process Calls the packet processing pipeline to convert the GIF into RGB565 packet files, a 16 × 16 preview GIF, and a JSON metadata file.
2 – Upload Sends the preview GIF, encoded payload, and metadata file to the backend storage endpoint.
3 – Register POSTs a design record to /designs, then links each uploaded file as a design asset via /design-assets.

Usage

python scripts/import_design.py --input path/to/animation.gif

Options

Flag Default Description
--input (required) Path to a GIF file or a folder of GIFs.
--output Input folder Output folder for processed files.
--backend-base-url http://127.0.0.1:8000 Backend base URL (overridden by BACKEND_BASE_URL env var).
--packet-size 120 Number of RGB565 hex values per packet.
--chunk-size 100 Number of packet files per chunk sub-folder.
--design-type gif Design type label written to the database.
--callsign (auto-generated) Fixed six-character callsign.
--creator (from metadata) Creator name override.
--description (from metadata) Description override.

Environment Variables

BACKEND_BASE_URL – overrides the default backend URL without needing to pass --backend-base-url on every invocation.

API Reference

scripts.import_design

End-to-end import script for ingesting a GIF design into the backend.

This script drives the full pipeline from a raw GIF file (or folder of GIFs) through to a fully registered design record with associated storage assets:

  1. Process – calls archive.process_packets.run_processing to convert each GIF into RGB565 packet files, a 16 × 16 preview GIF, and a JSON metadata file.
  2. Upload – sends the preview GIF, encoded payload, and metadata file to the backend storage endpoint.
  3. Register – POSTs a design record to /designs, then links each uploaded file as a design asset via /design-assets.

Typical usage::

python scripts/import_design.py --input path/to/animation.gif

See parse_args for the full list of command-line options.

build_payload(metadata, design_type, creator, description, callsign)

Assemble the JSON payload for the POST /designs endpoint.

Values supplied as arguments take precedence over those read from the metadata file, allowing the caller to override fields at import time.

Parameters:

Name Type Description Default
metadata dict

Parsed content of the *_meta.json file.

required
design_type str

Design type string (e.g. "gif").

required
creator str | None

Optional creator name; falls back to the metadata value.

required
description str | None

Optional description; falls back to the metadata value.

required
callsign str | None

Optional fixed callsign; a random one is generated if None.

required

Returns:

Type Description
dict

A dictionary suitable for serialising to JSON and posting to

dict

/designs.

Source code in scripts/import_design.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def build_payload(
    metadata: dict,
    design_type: str,
    creator: str | None,
    description: str | None,
    callsign: str | None,
) -> dict:
    """Assemble the JSON payload for the ``POST /designs`` endpoint.

    Values supplied as arguments take precedence over those read from the
    metadata file, allowing the caller to override fields at import time.

    Args:
        metadata: Parsed content of the ``*_meta.json`` file.
        design_type: Design type string (e.g. ``"gif"``).
        creator: Optional creator name; falls back to the metadata value.
        description: Optional description; falls back to the metadata value.
        callsign: Optional fixed callsign; a random one is generated if
            ``None``.

    Returns:
        A dictionary suitable for serialising to JSON and posting to
        ``/designs``.
    """
    payload = {
        "design_type": design_type,
        "gif_name": metadata["gif_name"],
        "callsign": callsign or generate_callsign(),
        "num_frames": int(metadata.get("num_frames", 0)),
        "num_packets": int(metadata.get("num_packets", 0)),
        "creator": creator if creator is not None else metadata.get("creator"),
        "description": description if description is not None else metadata.get("description"),
    }
    return payload

create_design_record(designs_url, payload)

Post a design payload and return the status, response, and echoed payload.

Parameters:

Name Type Description Default
designs_url str

Full URL of the /designs endpoint.

required
payload dict

Design fields to register.

required

Returns:

Type Description
tuple[int, str, dict]

A three-tuple of (http_status_code, response_body_text, payload).

Source code in scripts/import_design.py
208
209
210
211
212
213
214
215
216
217
218
219
def create_design_record(designs_url: str, payload: dict) -> tuple[int, str, dict]:
    """Post a design payload and return the status, response, and echoed payload.

    Args:
        designs_url: Full URL of the ``/designs`` endpoint.
        payload: Design fields to register.

    Returns:
        A three-tuple of ``(http_status_code, response_body_text, payload)``.
    """
    status, response = post_design(designs_url, payload)
    return status, response, payload

generate_callsign()

Generate a random six-character alphanumeric callsign.

Characters are drawn from uppercase ASCII letters and digits using the secrets module, making each callsign cryptographically random.

Returns:

Type Description
str

A six-character string such as "A3KZ2W".

Source code in scripts/import_design.py
45
46
47
48
49
50
51
52
53
54
def generate_callsign() -> str:
    """Generate a random six-character alphanumeric callsign.

    Characters are drawn from uppercase ASCII letters and digits using the
    ``secrets`` module, making each callsign cryptographically random.

    Returns:
        A six-character string such as ``"A3KZ2W"``.
    """
    return "".join(secrets.choice(CALLSIGN_ALPHABET) for _ in range(CALLSIGN_LENGTH))

load_metadata(metadata_path)

Load and parse a JSON metadata file produced by the processing step.

Parameters:

Name Type Description Default
metadata_path Path

Path to the *_meta.json file.

required

Returns:

Type Description
dict

The parsed metadata as a dictionary.

Source code in scripts/import_design.py
57
58
59
60
61
62
63
64
65
66
67
def load_metadata(metadata_path: Path) -> dict:
    """Load and parse a JSON metadata file produced by the processing step.

    Args:
        metadata_path: Path to the ``*_meta.json`` file.

    Returns:
        The parsed metadata as a dictionary.
    """
    with metadata_path.open("r") as file_handle:
        return json.load(file_handle)

parse_args()

Parse command-line arguments for the import script.

Returns:

Type Description
Namespace

A populated :class:argparse.Namespace with fields input,

Namespace

output, backend_base_url, packet_size, chunk_size,

Namespace

design_type, callsign, creator, and description.

Source code in scripts/import_design.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def parse_args() -> argparse.Namespace:
    """Parse command-line arguments for the import script.

    Returns:
        A populated :class:`argparse.Namespace` with fields ``input``,
        ``output``, ``backend_base_url``, ``packet_size``, ``chunk_size``,
        ``design_type``, ``callsign``, ``creator``, and ``description``.
    """
    parser = argparse.ArgumentParser(description="Process design input and ingest metadata via backend API.")
    parser.add_argument("--input", required=True, help="Path to a GIF file or a folder containing GIF files.")
    parser.add_argument("--output", help="Output folder for processed files. Defaults to input folder.")
    parser.add_argument("--backend-base-url", default=DEFAULT_BACKEND_BASE_URL, help="Backend base URL.")
    parser.add_argument("--packet-size", type=int, default=DEFAULT_PACKET_SIZE)
    parser.add_argument("--chunk-size", type=int, default=DEFAULT_CHUNK_SIZE)
    parser.add_argument("--design-type", default="gif")
    parser.add_argument("--callsign", help="Optional fixed callsign. If omitted, one is generated.")
    parser.add_argument("--creator", help="Optional creator override.")
    parser.add_argument("--description", help="Optional description override.")
    return parser.parse_args()

post_design(api_url, payload)

POST a design creation payload to the /designs endpoint.

Parameters:

Name Type Description Default
api_url str

Full URL of the /designs endpoint.

required
payload dict

Design fields as produced by :func:build_payload.

required

Returns:

Type Description
tuple[int, str]

A two-tuple of (http_status_code, response_body_text).

Source code in scripts/import_design.py
133
134
135
136
137
138
139
140
141
142
143
def post_design(api_url: str, payload: dict) -> tuple[int, str]:
    """POST a design creation payload to the ``/designs`` endpoint.

    Args:
        api_url: Full URL of the ``/designs`` endpoint.
        payload: Design fields as produced by :func:`build_payload`.

    Returns:
        A two-tuple of ``(http_status_code, response_body_text)``.
    """
    return post_json(api_url, payload)

post_design_asset(api_url, payload)

POST a design asset record to the /design-assets endpoint.

Parameters:

Name Type Description Default
api_url str

Full URL of the /design-assets endpoint.

required
payload dict

Asset fields including design_id, asset_type, and storage metadata.

required

Returns:

Type Description
tuple[int, str]

A two-tuple of (http_status_code, response_body_text).

Source code in scripts/import_design.py
194
195
196
197
198
199
200
201
202
203
204
205
def post_design_asset(api_url: str, payload: dict) -> tuple[int, str]:
    """POST a design asset record to the ``/design-assets`` endpoint.

    Args:
        api_url: Full URL of the ``/design-assets`` endpoint.
        payload: Asset fields including ``design_id``, ``asset_type``, and
            storage metadata.

    Returns:
        A two-tuple of ``(http_status_code, response_body_text)``.
    """
    return post_json(api_url, payload)

post_json(url, payload)

Send a JSON POST request using only the standard library.

Parameters:

Name Type Description Default
url str

The full URL to POST to.

required
payload dict

A JSON-serialisable dictionary to send as the request body.

required

Returns:

Type Description
tuple[int, str]

A two-tuple of (http_status_code, response_body_text).

Source code in scripts/import_design.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def post_json(url: str, payload: dict) -> tuple[int, str]:
    """Send a JSON POST request using only the standard library.

    Args:
        url: The full URL to POST to.
        payload: A JSON-serialisable dictionary to send as the request body.

    Returns:
        A two-tuple of ``(http_status_code, response_body_text)``.
    """
    body = json.dumps(payload).encode("utf-8")
    request = urllib.request.Request(
        url,
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(request) as response:
            response_body = response.read().decode("utf-8")
            return response.status, response_body
    except urllib.error.HTTPError as error:
        response_body = error.read().decode("utf-8")
        return error.code, response_body

upload_file_via_backend(backend_base_url, callsign, filename, file_path, content_type)

Upload a file to the backend storage endpoint.

Constructs a POST /storage/upload request, passing the callsign, remote filename, and content type as query parameters and the raw file bytes as the request body.

Parameters:

Name Type Description Default
backend_base_url str

Base URL of the backend (e.g. http://127.0.0.1:8000).

required
callsign str

The design callsign used to namespace the upload.

required
filename str

Remote filename to store the asset under.

required
file_path Path

Local path of the file to upload.

required
content_type str

MIME type of the file.

required

Returns:

Type Description
tuple[int, str]

A two-tuple of (http_status_code, response_body_text).

Source code in scripts/import_design.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def upload_file_via_backend(
    backend_base_url: str,
    callsign: str,
    filename: str,
    file_path: Path,
    content_type: str,
) -> tuple[int, str]:
    """Upload a file to the backend storage endpoint.

    Constructs a ``POST /storage/upload`` request, passing the callsign,
    remote filename, and content type as query parameters and the raw file
    bytes as the request body.

    Args:
        backend_base_url: Base URL of the backend (e.g. ``http://127.0.0.1:8000``).
        callsign: The design callsign used to namespace the upload.
        filename: Remote filename to store the asset under.
        file_path: Local path of the file to upload.
        content_type: MIME type of the file.

    Returns:
        A two-tuple of ``(http_status_code, response_body_text)``.
    """
    query = urllib.parse.urlencode(
        {
            "callsign": callsign,
            "filename": filename,
            "content_type": content_type,
        }
    )
    upload_url = f"{backend_base_url.rstrip('/')}/storage/upload?{query}"
    body = file_path.read_bytes()
    request = urllib.request.Request(
        upload_url,
        data=body,
        method="POST",
        headers={"Content-Type": "application/octet-stream"},
    )

    try:
        with urllib.request.urlopen(request) as response:
            response_body = response.read().decode("utf-8")
            return response.status, response_body
    except urllib.error.HTTPError as error:
        response_body = error.read().decode("utf-8")
        return error.code, response_body