Docs / Contact / sdks/python/getting-started
Python SDK: Getting Started
Go from zero to publishing device data in under a minute. The SDK handles batching, offline storage, heartbeats, and reconnection, so you just write your application logic.
Requirements: Python 3.9+
Install
The SDK installs straight from GitHub — with uv (recommended):
uv add git+https://github.com/tendrl-inc-labs/python-sdk
or with plain pip:
pip install git+https://github.com/tendrl-inc-labs/python-sdk
Either way the package is tendrl — the imports below work unchanged.
Every client needs an entity API key. If you haven't created one yet, create your first entity and copy its API key from the Connection Instructions dialog.
Your First Message (4 Lines)
from tendrl import Client
client = Client(api_key="your_api_key")
client.start()
client.publish({"temperature": 23.5, "humidity": 60}, tags=["sensor"])
That's it. The message is queued, batched, and delivered to Contact. Tags route it to your flows and connectors automatically.
Handling a Bad Key
If no key is supplied (and the TENDRL_KEY environment variable is unset), the constructor raises immediately:
from tendrl.client import Client, APIException
try:
client = Client(api_key=api_key)
except APIException as e:
print(f"Client could not start: {e}") # e.g. missing API key
raise
A key that is present but invalid is only detected when the client first contacts Contact: the publish fails as an authentication error rather than raising here. Run with Client(debug=True) during setup to print those send failures.
Automate with Tether
Most IoT work is "read a sensor, publish the result, repeat." The @tether decorator does exactly that: wrap any function and its return value is automatically published.
@client.tether(tags=["sensor", "environment"])
def read_sensors():
return {
"temperature": read_temp(),
"humidity": read_humidity(),
"pressure": read_pressure()
}
# Every call publishes the result; no manual publish() needed
while True:
read_sensors()
time.sleep(10)
Your function stays clean. The SDK handles serialization, batching, and delivery.
Tether with Offline Backup
Network goes down? Add write_offline=True and your data is stored locally in SQLite until connectivity returns:
@client.tether(tags=["critical"], write_offline=True, db_ttl=3600)
def critical_reading():
return {"voltage": 3.3, "current": 0.5}
No data loss. No retry logic to write. The SDK handles it.
Route Inbound Messages
Handle messages sent back from Contact flows or other entities. Route by tag or msg_type with @client.on():
client = Client(api_key="your_api_key")
@client.on(tag="diagnostic")
def run_diagnostics(message):
report = self_test(message.get("data", {}))
client.publish(report, tags=["diagnostic-result"])
@client.on(tag="ai-response")
def handle_ai_reply(message):
print("AI:", message.get("data", {}).get("response"))
@client.on(tags=["alert", "anomaly"])
def handle_alert(message):
print("Alert:", message.get("data"))
client.start()
Register specific routes before broad ones, because the first match wins. Pass callback= as a catch-all fallback for unmatched messages.
Tune polling with check_msg_rate (seconds between polls) and check_msg_limit (messages per poll).
Track Device State
Every entity has a persistent state table. Receive changes with @client.on_state(), which is polled at the same interval as messages:
@client.on_state()
def on_remote_state(state):
if state.get("status") == "needs_maintenance":
run_diagnostics()
To read or write state on demand, use the REST endpoints (see State Table).
What You Get for Free
When you call client.start(), the SDK automatically:
- Batches messages: groups publishes into efficient bulk sends
- Monitors connectivity: detects network changes every 30 seconds
- Sends heartbeats: reports memory and disk usage to Contact
- Stores offline messages: SQLite persistence when
offline_storage=True - Retries failed sends: exponential backoff, no data loss
- Adapts to load: dynamically sizes batches based on CPU and memory
You don't configure any of this. It just works.
Complete Example
from tendrl import Client
client = Client(api_key="your_api_key", offline_storage=True)
@client.on(tag="ai-response")
def on_ai_response(message):
print("AI:", message.get("data", {}).get("response"))
@client.tether(tags=["sensor"], write_offline=True)
def collect():
return {"temperature": 23.5, "humidity": 60}
client.start()
try:
while True:
collect()
time.sleep(10)
except KeyboardInterrupt:
client.stop()
Operating Modes
Direct API Mode (Default)
client = Client(mode="api", api_key="your_key")
Communicates directly with Contact over HTTP/2. Best for development and moderate throughput.
Nano Agent Mode
client = Client(mode="agent")
Routes messages through a local Nano Agent Unix socket. Best for production and high throughput (50+ msg/sec). Requires the Nano Agent running locally.
Headless Mode
client = Client(api_key="your_key", headless=True)
No background threads. Every publish() sends immediately and returns the response. Use for simple scripts or one-off sends.
What's Next
- Configuration: All config options and tuning
- Examples: Raspberry Pi, command handling, and more
- API Reference: Full method documentation
Tendrl