Skip to main content

struct-sdk

OpenTelemetry instrumentation for AI agents in Python. Captures spans, token usage, and message events from the Anthropic SDK, the OpenAI SDK (Responses API), pydantic-ai, the Claude Agent SDK, and LangChain / LangGraph, and exports them to struct.ai for observability.

A TypeScript version is available as @struct-ai/sdk. Both SDKs produce structurally identical traces, so a single agent system can mix languages without a fragmented view.

Install

pip install struct-sdk
# optional — the SDK auto-instruments these if present
pip install anthropic
pip install openai
pip install pydantic-ai-slim
pip install claude-agent-sdk
pip install langchain-core langgraph

Requires Python 3.10+.

Quickstart

Get an ingest key from app.struct.ai/settings?tab=ingest-keys, then call struct.init() once at startup and wrap your agent loop:

import os
from struct_sdk import struct

struct.init(
    ingest_key=os.environ["STRUCT_INGEST_KEY"],  # or pass the string directly
    service_name="my-agent",
    environment="production",
)

import anthropic
client = anthropic.AsyncAnthropic()

# Decorate each tool — auto-captures arguments + result + tool_call_id.
@struct.tool()
async def search(query: str):
    ...

async with struct.agent(name="checkout"):
    msg = await client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        messages=[{"role": "user", "content": "plan my checkout flow"}],
    )
    result = await search(query="...")

What gets traced

Library Span type Notes
anthropic chat {model} Cache-token accounting; streaming chats with tool-use reconstruction. Bedrock and Vertex variants supported if installed.
openai (Responses API) chat {model} Non-streaming responses.create on sync and async clients (Azure clients included). Cached-token accounting from usage.input_tokens_details. Streaming and chat.completions calls pass through untraced. Message-content capture at parity with the Anthropic integration: per-message events (gen_ai.system/user/assistant/tool.message) and gen_ai.choice, plus gen_ai.input.messages / gen_ai.output.messages in span-content mode. Reasoning items are skipped (encrypted_content is never emitted).
pydantic_ai invoke_agent {agent}, chat {model}, execute_tool {tool} pydantic-ai's own instrumentation, turned on with Struct's tracer provider. See pydantic-ai.
claude_agent_sdk agent, chat, execute_tool Telemetry comes from Claude Code itself in the subprocess; ingest credentials are propagated to it via ClaudeAgentOptions. Subagents inherit the configuration.
langchain_core BaseChatModel chat {model} When the underlying provider SDK is also instrumented (e.g. ChatAnthropic + anthropic), the provider's span is suppressed, so there is a single span.
langchain_core BaseTool execute_tool {name} tool_call_id extracted from the LangChain ToolCall when present.
langchain_core BaseRetriever retrieval {name}
langgraph Pregel invoke_agent {name} Covers create_react_agent, langchain.agents.create_agent, and custom graphs. Reads conversation id from any of: configurable.thread_id (LangGraph canonical), or metadata.{thread_id, session_id, conversation_id} (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in struct.agent(session_id=conv_id) — that's the struct-native replacement for ls.tracing_context(parent=run_tree).

Framework integration

struct.init() takes the same parameters regardless of which framework you're instrumenting. Required: ingest_key (get one at app.struct.ai/settings?tab=ingest-keys). Recommended: service_name, environment.

What you need to do beyond init() depends on whether you're using an agent framework (which has built-in concepts of agents and tools) or an LLM SDK directly (which only knows about chat completions). The SDK auto-instruments both, but only agent frameworks get full agent + tool spans for free — when you call an LLM SDK directly, you have to tell the SDK where the agent and tool boundaries are.

Call init() once, as early as possible, before the instrumented libraries are imported.

Agent frameworks — fully auto-instrumented

For these, calling struct.init() is the only setup. Agent, tool, chat, and retrieval spans all emit automatically.

Claude Agent SDK

from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="claude-agent")

from claude_agent_sdk import ClaudeAgentOptions, query
# Telemetry is generated by Claude Code itself in the subprocess and
# exported directly via OTLP. struct.init() configures the ingest
# credentials on ClaudeAgentOptions; subagents inherit them automatically.

pydantic-ai

from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="support-agent")

from pydantic_ai import Agent
agent = Agent("openai:gpt-5.5", name="support")
result = await agent.run("Refund order 42", conversation_id="thread-42")

pydantic-ai already emits OpenTelemetry GenAI spans; struct.init() turns that on for every agent (Agent.instrument_all) with Struct's tracer provider, unless you already set your own default (for example with logfire). You get pydantic-ai's invoke_agent, chat and execute_tool spans, with one chat span per model call (Struct's OpenAI and Anthropic patches stay out of the way for pydantic-ai's own provider calls; model calls your code makes while reading a stream keep their spans). During agent runs, Struct guards telemetry faults and caps content. Realtime sessions and direct InstrumentedModel exception recording are outside this guarantee.

  • Content goes on span attributes only (pydantic-ai has no message events): span_only / span_and_event include prompts, answers and tool arguments; event_only and none do not. Tool definitions are always recorded.
  • Sessions: on Struct's provider, a struct.agent(session_id=...) around the run wins; otherwise pydantic-ai's conversation_id is used. Instrumentation configured on your own provider keeps its conversation IDs.
  • Turn it off with struct.init(..., instrumentation_options={"pydantic_ai": {"enabled": False}}), or for one agent with agent.instrument = False. An Agent.instrument_all(False) before struct.init() is pydantic-ai's initial value, so it does not keep pydantic-ai off.

LangChain / LangGraph (with an agent or graph)

from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="my-graph")

from langgraph.prebuilt import create_react_agent
# Pregel / CompiledStateGraph / AgentExecutor invocations get invoke_agent
# spans. BaseChatModel calls get chat spans. BaseTool.invoke gets
# execute_tool spans. BaseRetriever.invoke gets retrieval spans.
Recommended pattern: wrap LangChain entry points in struct.agent

For multi-turn HTTP-style usage (every request continues the same conversation), wrap your request handler in struct.agent(session_id=conversation_id). This is the struct-native replacement for with ls.tracing_context(parent=run_tree): and gives you two things you can't get from configurable.thread_id alone:

  1. Threading without per-call config plumbing. Every nested LangChain call inherits the conversation id via the SDK's ambient contextvar — you don't have to ensure each compiled_graph.ainvoke gets thread_id on its config.
  2. One trace per request. struct.agent creates a parent OTel span so all the LangChain work for the request nests under one trace (clean tree, "Subagents" / "Spawned by" UI links work). Without it, each .invoke() becomes its own root trace, and the UI's session list shows a non-deterministic agent name (omni_agent, LangGraph, the first sub-agent it sees…).

Migrating from LangSmith:

# Before — LangSmith convention, fragments under struct-sdk
with ls.tracing_context(parent=run_tree):
    await orchestrator.ainvoke(inputs, config=config)

# After — struct-native, threads correctly, no langsmith dep
async with struct.agent(session_id=conversation_id):
    await orchestrator.ainvoke(inputs, config=config)

Also works as a sync context manager (with struct.agent(...)) for non-async handlers.

LLM SDKs used directly — manual agent + tool scopes required

When you call an LLM SDK directly (no agent framework wrapping it), only chat spans emit automatically. You need to wrap your agent loop in struct.agent() and each tool execution in struct.tool() so the SDK knows where to put the agent and tool boundaries — otherwise you'll see free-floating chat spans with no agent or tool context around them.

Anthropic SDK (raw)

from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="checkout-agent")

import anthropic
client = anthropic.AsyncAnthropic()

# Recommended: define each tool as a function and DECORATE it. The decorator
# auto-captures the tool's arguments + result on the execute_tool span and
# auto-fills tool_call_id from the preceding Anthropic response — no manual
# bookkeeping.
@struct.tool()
async def search(query: str):
    ...

# Required: wrap the agent loop yourself.
async with struct.agent(name="checkout"):
    msg = await client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        messages=[...],
    )
    # Dispatching a decorated tool inside the agent emits a fully-populated
    # execute_tool span (name, id, arguments, result):
    result = await search(query="...")

For dynamic dispatch (the LLM picks a tool from a registry at runtime), apply the decorator at runtime — still automatic, just bind the name when you wrap the callable:

registry = {t.name: struct.tool(name=t.name)(t.execute) for t in tools}
result = await registry[block.name](**block.input)   # arguments + result captured

struct.tool() can also be used as a context manager (async with struct.tool(name=...): ...) to instrument an arbitrary block of code as a tool span. That form is a manual escape hatch — it does NOT auto-capture arguments/result (a with block can't see the body's return value), so prefer the decorator for actual tool calls. See Parallel tool calls for the one runtime value (tool_call_id) you must supply under concurrency.

anthropic.Anthropic, anthropic.AsyncAnthropic, and the bedrock/vertex clients are all auto-instrumented for chat spans.

Parallel tool calls — pass tool_call_id explicitly

When you execute an assistant turn's tool calls sequentially — one await at a time, in the order the tool_use blocks appear — struct.tool() auto-fills gen_ai.tool.call.id by matching each span to the next pending tool_use of the same tool name. Nothing extra to do.

When you execute them concurrently (e.g. asyncio.gather), that name-and-order matching is ambiguous: two struct.tool(name="search") spans can start in any order, so the auto-fill may attach the wrong id (and thus the wrong arguments/result) to a call. In that case pass tool_call_id explicitly from the originating tool_use block — an explicit id always overrides the auto-linkage:

async def run_one(block):
    # The id from THIS block overrides the name/order auto-fill.
    async with struct.tool(name=block.name, tool_call_id=block.id):
        return await dispatch(block.name, **block.input)

# Concurrent execution — each tool span still carries the correct id.
results = await asyncio.gather(*[run_one(b) for b in tool_use_blocks])

Rule of thumb: serial tool execution → automatic; concurrent tool execution → provide tool_call_id= yourself. (Auto-instrumented frameworks such as LangChain read the id from the framework's ToolCall, so this only applies when you drive the tool loop directly against an LLM SDK.)

LangChain BaseChatModel (no agent/graph)

If you call ChatAnthropic.invoke(...) (or any other BaseChatModel) without wrapping it in an AgentExecutor or LangGraph, only the chat span emits automatically. Same rule as raw Anthropic — wrap your agent loop in struct.agent() and tool execution in struct.tool().

from struct_sdk import struct
struct.init(ingest_key="pk-...", service_name="my-agent")

from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")

async with struct.agent(name="my-agent"):
    response = await llm.ainvoke([("user", "...")])
    async with struct.tool(name="search"):
        ...

When you do use ChatAnthropic and have the anthropic SDK installed, the chat span comes from the LangChain layer (single span); the Anthropic instrumentation skips its own to avoid duplicates.

Content capture

The SDK supports four capture modes controlling how prompt/response content is emitted.

from struct_sdk import struct, ContentCaptureMode

struct.init(
    ingest_key=...,
    content_capture=ContentCaptureMode.EVENT_ONLY,  # opt in; the default is NONE
    # or ContentCaptureMode.SPAN_ONLY, SPAN_AND_EVENT
)
  • EVENT_ONLY: per-message content lands on OTel log records (gen_ai.{user,assistant,system,tool}.message, gen_ai.choice). Spans carry metadata only.
  • SPAN_ONLY: content on span attributes (gen_ai.input.messages, gen_ai.output.messages).
  • SPAN_AND_EVENT: both.
  • NONE (default since 0.3.0): no content captured, including the user prompt on the agent span and tool arguments and results. Token counts, tool call IDs, finish reasons, and other metadata still flow.

capture_content=False is a shorthand for ContentCaptureMode.NONE, and capture_content=True for EVENT_ONLY.

Manual scopes

struct.agent() and struct.tool() create invoke_agent and execute_tool spans. Use them when you call an LLM SDK directly; you don't need them when an agent framework (LangGraph, AgentExecutor, Claude Agent SDK) is already creating those spans for you.

async with struct.agent(
    name="onboarding",
    session_id=conversation_id,
    metadata={"tenant": "acme"},
):
    async with struct.tool(name="fetch-profile"):
        return await fetch_profile()

Decorator form:

@struct.agent(name="checkout")
async def run_checkout(order_id: str):
    @struct.tool(name="charge-card")
    async def charge():
        return await stripe.charge(order_id)

    return await charge()

Sub-agents (e.g. a create_agent graph invoked from inside another agent's tool body) record their parent via the struct.agent.parent_session_id attribute on the inner invoke_agent span. This powers the UI's "Spawned by" backlink, which works for any nested invocation.

The parent's "Subagents" forward list — the inverse direction — additionally requires that the nested invoke shares the outer agent's trace. This works automatically when the outer tool is built with the @tool decorator (or any callback-aware wrapping) since the nested invoke inherits the parent's run state. Bare Tool(...) constructors that bypass the callback chain can break the forward link; the backlink still renders.

Semantic conventions

Emits attributes per the OTel GenAI semantic conventions:

  • gen_ai.operation.name — chat, execute_tool, invoke_agent, retrieval
  • gen_ai.provider.name — the GenAI provider on inference spans (anthropic, openai, …); platform-routed calls report the platform value (aws.bedrock, gcp.vertex_ai, azure.ai.openai) when detectable from the client. invoke_agent spans inherit the provider from their first child inference call (omitted when no model is reached); execute_tool/retrieval spans carry no provider.
  • gen_ai.request.{model, max_tokens, temperature, top_p, top_k, stop_sequences}
  • gen_ai.response.{model, id, finish_reasons}
  • gen_ai.usage.{input_tokens, output_tokens, cache_read.input_tokens, cache_creation.input_tokens}
  • gen_ai.conversation.id
  • gen_ai.tool.{name, call.id, call.arguments, call.result}
  • error.type + StatusCode.ERROR on failures

error.type values emitted

The OTel conventions ask instrumentations to document the error values they report ("Instrumentations SHOULD document the list of errors they report"). This SDK emits exactly two kinds of value, both alongside span StatusCode.ERROR:

Value When Meaning
exception class name (e.g. RuntimeError, httpx.ConnectError) the instrumented call raised We failed to execute the request. Also records an OTel exception event.
tool_error an execute_tool span whose result signalled failure in band — Anthropic tool_result blocks with is_error: true, MCP CallToolResult.isError, or a LangChain ToolMessage with status="error" The tool ran and reported a failure back to the model (bad arguments, a domain "no"), so the model can self-correct. No exception object exists, so no class name is available.

tool_error is a deliberate low-cardinality sentinel, which the error.type convention explicitly permits ("another low-cardinality error identifier"; a custom value MAY be used where no well-known one applies). The same split is used by OpenTelemetry's MCP instrumentation in OpenLLMetry, which likewise reports error.type="tool_error" for the isError path and the exception class name otherwise.

The distinction is what lets a monitor page on genuine execution failures while excluding failures the model already saw and can recover from.

Note: gen_ai.usage.input_tokens for Anthropic is the true total — the SDK adds back cache_read_input_tokens and cache_creation_input_tokens, which Anthropic's raw response excludes from input_tokens.

Configuration

struct.init(
    ingest_key="pk-...",                # required
    service_name="my-agent",            # default: OTEL_SERVICE_NAME, else "default-agent"
    service_version="1.2.3",            # default: from OTEL_RESOURCE_ATTRIBUTES, else "0.0.0"
    environment="production",           # default: from OTEL_RESOURCE_ATTRIBUTES, else "development"
    endpoint="https://ingest.struct.ai", # default; override for self-hosted
    shutdown_timeout_seconds=5.0,        # default: 5.0
    content_capture=ContentCaptureMode.EVENT_ONLY,  # default: NONE
    auto_instrument=True,                # default; False = AI libraries only
    instrumentation_options=None,        # per-library full-app settings
    redact_attributes=["*.password"],    # default: STRUCT_REDACT_ATTRIBUTES
)

With auto_instrument=False the SDK uses an isolated TracerProvider and LoggerProvider and instruments only the AI libraries; your existing OTel setup is unaffected. The default is full-app mode.

Service identity follows the standard OpenTelemetry variables when you don't pass it: OTEL_SERVICE_NAME, and service.version and deployment.environment in OTEL_RESOURCE_ATTRIBUTES. Arguments to init() win. The resource also carries host, runtime and (with the [auto] extra) container metadata, plus the commit when STRUCT_GIT_COMMIT_SHA and STRUCT_GIT_REPOSITORY_URL are set. It includes a service.instance.id so replicas stay distinguishable; an ID from OTEL_RESOURCE_ATTRIBUTES is preserved.

In pre-fork servers (for example gunicorn --preload), call struct.init() in each worker (gunicorn's post_fork hook), not in the parent: forked workers would otherwise share the parent's instance id. A worker that exports after being forked from an initialized parent logs one INFO line saying so.

Redaction

redact_attributes takes glob patterns, matched case-insensitively. Span, span-event and log attributes whose keys match leave the process as [REDACTED], for example ["http.request.header.authorization", "*.password"]. STRUCT_REDACT_ATTRIBUTES (comma-separated) sets them without code. Only what Struct sends is changed; your own exporters still see the real values. Metrics, log bodies and span names are not redacted.

Full-app mode

Full-app mode is the default since 0.3.0: struct.init() adds APM-style telemetry for the whole service, like an OpenTelemetry distro: HTTP servers and clients, databases, caches, queues, app logs and process metrics, built on the OpenTelemetry contrib instrumentors. Every supported library you have installed is instrumented; turn individual ones off below. To keep the 0.2 behavior (AI libraries only, isolated providers), pass auto_instrument=False.

pip install "struct-sdk[auto]"           # needs opentelemetry-sdk 1.30+; add [azure] for Azure SDK tracing,
                                         # [structlog] for structlog (opentelemetry-sdk 1.44+),
                                         # [exceptions] for uncaught exceptions (1.43+)
import fastapi
from struct_sdk import struct

struct.init(ingest_key="pk-...", service_name="checkout-api")

app = fastapi.FastAPI()  # resolve the class after init()

Framework instrumentors replace the framework's class when init() runs, so an app class bound earlier stays uninstrumented: from fastapi import FastAPI above struct.init() keeps the original class. Import framework classes after init(), or reference them through the module as above. For an app created before init(), call the contrib instrumentor directly (for example FastAPIInstrumentor.instrument_app(app)). In pre-fork servers, run init() in each worker, as described above.

Signal Coverage
Traces Django, FastAPI, Flask, Starlette, Tornado, Falcon, Pyramid, aiohttp (client and server), requests, httpx, urllib, urllib3, gRPC, psycopg2, psycopg, aiopg, asyncpg, PyMySQL, MySQL Connector, mysqlclient, pymssql, sqlite3, PyMongo, Cassandra, Redis, pymemcache, Tortoise ORM, Celery, botocore, aiobotocore, Kafka (confluent-kafka, aiokafka, kafka-python), RabbitMQ (pika, aio-pika), Jinja2, Azure SDK. Work handed to threads and ThreadPoolExecutor stays in the caller's trace. SQLAlchemy is off by default because its query spans duplicate the database driver's; enable it with {"sqlalchemy": {"enabled": True}} if you prefer ORM-level spans.
Logs Python logging records that reach the root logger, with trace context. The SDK's own and OpenTelemetry's records are excluded. With the [structlog] extra, structlog events that structlog writes itself (its default) are exported with their fields as attributes; events structlog hands to logging (structlog.stdlib.LoggerFactory) are exported once, through the logging bridge. Extras and structlog fields OpenTelemetry can't take as-is are exported as strings: dicts and mixed-type lists as JSON, everything else (UUIDs, datetimes, None, bytes) as its str(). Your other handlers still see the original values. With the [exceptions] extra, uncaught exceptions (main thread, other threads, asyncio) are exported as log records. If your app configures no logging, its warnings still print to stderr as they would without Struct. This is separate from content_capture, which only governs LLM content; disable {"logging": {"enabled": False}} and {"structlog": {"enabled": False}} (which also stops copies of your logs when Struct attaches to your OpenTelemetry setup), or redact attributes, if application logs can carry sensitive data.
Metrics Instrumentor metrics (for example http.server.duration) and process metrics (process.cpu.*, process.memory.*, threads, file descriptors, GC). Counters and histograms use delta temporality.

Each instrumentor activates only when its library is installed. instrumentation_options disables or configures one by name; other keys are passed to that instrumentor:

struct.init(
    ingest_key="pk-...",
    instrumentation_options={
        "fastapi": {"excluded_urls": "health,ready"},
        "logging": {"enabled": False},
    },
)

As with opentelemetry-instrument and the Azure Monitor distro, the standard OTEL_PYTHON_DISABLED_INSTRUMENTATIONS environment variable also turns libraries off (OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=requests,urllib3, or * for all of them). An explicit instrumentation_options entry takes precedence over it.

Full-app mode installs Struct's providers as the global OpenTelemetry providers only when no tracer, meter or logger provider is installed. If the process already configured any of them (Azure Monitor, your own SDK setup), Struct adds its exporters to your SDK tracer and logger providers and instruments nothing: your setup keeps ownership of instrumentation, sampling and shutdown, and your metrics are not sent to Struct. Struct's own GenAI spans keep their isolated provider either way, so your sampler does not drop them.

Datadog ddtrace

Under ddtrace-run, Struct leaves the libraries ddtrace patches to it, and urllib3 beneath requests or botocore, to avoid duplicate spans and conflicting trace headers, and logs one INFO line listing them. Logs, process metrics, thread context, AI spans and libraries ddtrace doesn't patch still go to Struct. With DD_TRACE_OTEL_ENABLED=true, ddtrace's OpenTelemetry provider isn't the OpenTelemetry SDK, so full-app mode attaches nothing (an INFO line says so) and only AI spans reach Struct.

Zero-code activation

OTEL_PYTHON_DISTRO=struct \
STRUCT_INGEST_KEY=pk-... \
OTEL_SERVICE_NAME=checkout-api \
opentelemetry-instrument python app.py

opentelemetry-instrument comes with [auto]. Struct calls struct.init() before your app starts, with STRUCT_INGEST_URL as the endpoint when set. Message content stays off; call struct.init() in code to turn it on. Installing the SDK never changes another distro: Struct's distro is used only when OTEL_PYTHON_DISTRO=struct. When selected, it also keeps other installed configurators (such as opentelemetry-distro's) from installing competing providers, unless you set OTEL_PYTHON_CONFIGURATOR yourself. If you already run another distro, call struct.init() in code instead and Struct attaches to its providers.

Temporal

pip install "struct-sdk[temporal]"
from temporalio.client import Client
from temporalio.runtime import Runtime
from struct_sdk import struct, temporal

struct.init(ingest_key="pk-...", service_name="billing-worker")
runtime = Runtime(telemetry=temporal.telemetry_config(global_tags={"replica": replica_id}))
client = await Client.connect(
    "temporal:7233",
    runtime=runtime,
    interceptors=[temporal.tracing_interceptor()],
)

telemetry_config() exports Temporal Core metrics (task slots, schedule-to-start latency, poll results, sticky cache) to Struct. It keeps Core's console logging at Temporal's defaults, except that failed metric exports are not printed. tracing_interceptor() (temporalio 1.11+) traces workflows and activities with Struct's tracer; a failing span processor never blocks a Temporal call or replaces its exception. Temporal server metrics are a Prometheus endpoint; scrape them with an OpenTelemetry Collector and export OTLP/HTTP to your Struct ingest endpoint with the x-struct-ingest-key header.

Reliability

The SDK is designed never to break your application. Instrumentation hooks, span exports, and shutdown all swallow exceptions internally and log at DEBUG on the struct_sdk logger, with a traceback for the first failure at each site. Process exit is bounded by shutdown_timeout_seconds and runs in a background thread, so a slow or unreachable ingest endpoint cannot hang shutdown.

Troubleshooting

  • Spans missing after instrumenting: Call struct.init() before importing the instrumented libraries, so the SDK can wire up instrumentation before any instance is constructed.
  • No message log records appearing: they only emit when the capture mode is EVENT_ONLY or SPAN_AND_EVENT. The default is NONE.
  • Duplicate chat spans: When an LLM provider SDK and a wrapping framework are both instrumented (e.g. ChatAnthropic calling through to anthropic), the provider-level chat span is skipped and the framework's is kept.

License

Apache-2.0

Metadata

Release files for struct-sdk 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for struct-sdk 0.3.2
File Size Uploaded Provenance Actions
struct_sdk-0.3.2.tar.gz 121.1 kB With provenance Details for struct_sdk-0.3.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for struct-sdk 0.3.2
File Interpreter ABI Platform Actions
struct_sdk-0.3.2-py3-none-any.whl Python 3 none any Details for struct_sdk-0.3.2-py3-none-any.whl

Total release size: 238.4 kB

Release files / struct_sdk-0.3.2.tar.gz

Download URL struct_sdk-0.3.2.tar.gz
Size 121.1 kB
Tags Source
SHA-256 checksum
How to use checksums
01386a78452768d720675ad0f9e3f99a78c121314309b0028d45d8f03eb3dc69
BLAKE2b-256 checksum
How to use checksums
bd9eca8236e0aead295dcb758d89c6cdb1a785215a5914c24df3add59864eaa5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Release files / struct_sdk-0.3.2-py3-none-any.whl

Download URL struct_sdk-0.3.2-py3-none-any.whl
Size 117.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4329b4697ae58964a0233f914b3ccb15463e1718fb1742ac513316245c876761
BLAKE2b-256 checksum
How to use checksums
361dba5e35961b510297082c79efc69411b332089011ab6532e936051ffbf411
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Security for struct-sdk 0.3.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for struct_sdk-0.3.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.2 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page