artifact demo

This commit is contained in:
2026-03-02 15:56:22 -06:00
parent daf3d04395
commit 9414ee34e2
2 changed files with 362 additions and 0 deletions

294
actions/artifact_demo.py Normal file
View File

@@ -0,0 +1,294 @@
#!/usr/bin/env python3
"""
Artifact Demo Action - Python Example Pack
Demonstrates creating file and progress artifacts via the Attune API.
Each iteration:
1. Appends a timestamped log line to a file artifact (via version upload)
2. Updates a progress artifact by 2%
3. Sleeps for 0.5 seconds
The action authenticates to the API using the provided credentials (or defaults)
and uses ATTUNE_API_URL / ATTUNE_EXEC_ID environment variables set by the worker.
"""
import json
import os
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
def api_request(
base_url, path, method="GET", data=None, token=None, content_type="application/json"
):
"""Make an HTTP request to the Attune API."""
url = f"{base_url}{path}"
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
body = None
if data is not None:
if content_type == "application/json":
body = json.dumps(data).encode("utf-8")
headers["Content-Type"] = "application/json"
else:
body = data if isinstance(data, bytes) else data.encode("utf-8")
headers["Content-Type"] = content_type
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8")), resp.status
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")
print(f"API error {e.code} on {method} {path}: {error_body}", file=sys.stderr)
raise
def multipart_upload(
base_url, path, file_bytes, filename, token, content_type="text/plain"
):
"""Upload a file via multipart/form-data."""
boundary = f"----AttuneArtifact{int(time.time() * 1000)}"
body_parts = []
# file field
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(
f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
)
body_parts.append(f"Content-Type: {content_type}\r\n\r\n".encode())
body_parts.append(file_bytes)
body_parts.append(b"\r\n")
# content_type field
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="content_type"\r\n\r\n')
body_parts.append(content_type.encode())
body_parts.append(b"\r\n")
# created_by field
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="created_by"\r\n\r\n')
body_parts.append(b"python_example.artifact_demo")
body_parts.append(b"\r\n")
body_parts.append(f"--{boundary}--\r\n".encode())
full_body = b"".join(body_parts)
url = f"{base_url}{path}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Accept": "application/json",
}
req = urllib.request.Request(url, data=full_body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8")), resp.status
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")
print(f"Upload error {e.code} on {path}: {error_body}", file=sys.stderr)
raise
def login(base_url, username, password):
"""Authenticate and return a JWT token."""
data, _ = api_request(
base_url,
"/auth/login",
method="POST",
data={
"username": username,
"password": password,
},
)
return data["data"]["access_token"]
def create_artifact(
base_url,
token,
ref,
name,
artifact_type,
execution_id,
content_type=None,
description=None,
data=None,
):
"""Create a new artifact and return its ID."""
payload = {
"ref": ref,
"scope": "action",
"owner": "python_example.artifact_demo",
"type": artifact_type,
"retention_policy": "versions",
"retention_limit": 10,
"name": name,
"execution": execution_id,
}
if content_type:
payload["content_type"] = content_type
if description:
payload["description"] = description
if data is not None:
payload["data"] = data
resp, _ = api_request(
base_url, "/api/v1/artifacts", method="POST", data=payload, token=token
)
return resp["data"]["id"]
def append_progress(base_url, token, artifact_id, entry):
"""Append an entry to a progress artifact."""
api_request(
base_url,
f"/api/v1/artifacts/{artifact_id}/progress",
method="POST",
data={"entry": entry},
token=token,
)
def main():
start_time = time.time()
try:
# Read parameters from stdin (JSON format)
params = json.loads(sys.stdin.readline())
iterations = params.get("iterations", 50)
username = params.get("username") or os.environ.get(
"ATTUNE_USERNAME", "test@attune.local"
)
password = params.get("password") or os.environ.get(
"ATTUNE_PASSWORD", "TestPass123!"
)
# Get execution context from environment
api_url = os.environ.get("ATTUNE_API_URL", "http://localhost:8080")
exec_id_str = os.environ.get("ATTUNE_EXEC_ID", "")
execution_id = int(exec_id_str) if exec_id_str else None
print(
f"Artifact demo starting: {iterations} iterations, API at {api_url}",
file=sys.stderr,
)
# Authenticate
token = login(api_url, username, password)
print("Authenticated successfully", file=sys.stderr)
# Build unique artifact refs using execution ID to avoid collisions
ts = int(time.time())
ref_suffix = f"{execution_id}_{ts}" if execution_id else str(ts)
file_ref = f"python_example.artifact_demo.log.{ref_suffix}"
progress_ref = f"python_example.artifact_demo.progress.{ref_suffix}"
# Create file artifact (file_text type)
file_artifact_id = create_artifact(
base_url=api_url,
token=token,
ref=file_ref,
name="Artifact Demo Log",
artifact_type="file_text",
execution_id=execution_id,
content_type="text/plain",
description=f"Log output from artifact demo ({iterations} iterations)",
)
print(
f"Created file artifact ID={file_artifact_id} ref={file_ref}",
file=sys.stderr,
)
# Create progress artifact
progress_artifact_id = create_artifact(
base_url=api_url,
token=token,
ref=progress_ref,
name="Artifact Demo Progress",
artifact_type="progress",
execution_id=execution_id,
description=f"Progress tracker for artifact demo ({iterations} iterations)",
data=[], # Initialize with empty array
)
print(
f"Created progress artifact ID={progress_artifact_id} ref={progress_ref}",
file=sys.stderr,
)
# Run iterations
log_lines = []
for i in range(iterations):
iteration = i + 1
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
pct = min(round(iteration * (100.0 / iterations), 1), 100.0)
# Build log line
log_line = f"[{now}] Iteration {iteration}/{iterations} — progress {pct}%"
log_lines.append(log_line)
print(f" Iteration {iteration}/{iterations} ({pct}%)", file=sys.stderr)
# Upload the full log as a new file version
full_log = "\n".join(log_lines) + "\n"
multipart_upload(
base_url=api_url,
path=f"/api/v1/artifacts/{file_artifact_id}/versions/upload",
file_bytes=full_log.encode("utf-8"),
filename="artifact_demo.log",
token=token,
content_type="text/plain",
)
# Append progress entry
append_progress(
api_url,
token,
progress_artifact_id,
{
"iteration": iteration,
"total": iterations,
"percent": pct,
"message": f"Completed iteration {iteration}",
"timestamp": now,
},
)
# Sleep between iterations
if iteration < iterations:
time.sleep(0.5)
elapsed = round(time.time() - start_time, 3)
result = {
"file_artifact_id": file_artifact_id,
"progress_artifact_id": progress_artifact_id,
"iterations_completed": iterations,
"elapsed_seconds": elapsed,
"success": True,
}
print(json.dumps(result))
print(f"Artifact demo completed in {elapsed}s", file=sys.stderr)
return 0
except Exception as e:
elapsed = round(time.time() - start_time, 3)
error_result = {
"success": False,
"error": str(e),
"elapsed_seconds": elapsed,
}
print(json.dumps(error_result))
print(f"ERROR: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,68 @@
# Artifact Demo Action
# Demonstrates creating file and progress artifacts via the Attune API
ref: python_example.artifact_demo
label: "Artifact Demo"
description: "Creates a file artifact and a progress artifact, writing lines and updating progress over multiple iterations"
enabled: true
# Runner type determines how the action is executed
runner_type: python
# Minimum Python version required (semver constraint)
runtime_version: ">=3.9"
# Entry point is the Python script to execute
entry_point: artifact_demo.py
# Parameter delivery: stdin for secure parameter passing
parameter_delivery: stdin
parameter_format: json
# Output format: json (structured data parsing enabled)
output_format: json
# Action parameters schema (flat format with inline required/secret)
parameters:
iterations:
type: integer
description: "Number of iterations to run (each adds a log line and 2% progress)"
default: 50
minimum: 1
maximum: 200
username:
type: string
description: "API username for authentication (defaults to ATTUNE_USERNAME env var or test@attune.local)"
default: "test@attune.local"
password:
type: string
description: "API password for authentication (defaults to ATTUNE_PASSWORD env var or TestPass123!)"
secret: true
default: "TestPass123!"
# Output schema (flat format)
output_schema:
file_artifact_id:
type: integer
description: "ID of the created file artifact"
required: true
progress_artifact_id:
type: integer
description: "ID of the created progress artifact"
required: true
iterations_completed:
type: integer
description: "Number of iterations completed"
required: true
success:
type: boolean
description: "Whether the demo completed successfully"
required: true
# Tags for categorization
tags:
- python
- example
- artifacts
- progress
- demo