Skip to content

Python SDK ​

Overview ​

The Urban Sky Python SDK provides real-time access to balloon telemetry data. It connects to our WebSocket service and delivers location updates for balloons in your organization.

Prerequisites ​

Requirements:

  • Python 3.8+ - For proper async/await support
  • requests - Needed to fetch the loader (pip install requests)

Note: Real-time connectivity is built into the SDK. HTTP requests use Python built-in modules (urllib.request, urllib.error), but the WebSocket transport requires centrifuge-python>=0.3.0 — a hard dependency that must be available in your environment (pip install centrifuge-python).

Installation ​

Load the SDK using our CDN loader:

python
import requests

# Load the SDK dynamically — always pulls the latest version
exec(requests.get('https://sdk.atmosys.com/runtime/py/current/loader.py').text)

# UrbanSkySDK is now available

Basic Usage ​

Initialization and Connection ​

python
import asyncio
import os

async def main():
    try:
        # Initialize and connect in one step
        sdk = await UrbanSkySDK.init({
            'apiToken': os.getenv('URBAN_SKY_API_TOKEN')
            # Most configuration options have sensible defaults
        })
        print('Connected to Urban Sky!')
    except Exception as error:
        print(f'Connection failed: {error}')

Listening for Balloon Updates ​

The SDK emits balloon:update events when balloon positions change:

The payload is a BalloonUpdate dataclass with snake_case attributes — access it with attribute syntax, not dict subscripts:

python
def handle_balloon_update(update):
    print(f"Balloon {update.balloon_id} update:")

    for device in update.devices:
        print(f"  Device {device.device_id}:")
        print(f"    Location: {device.lat}, {device.lng}")
        print(f"    Timestamp: {device.timestamp}")

sdk.on('balloon:update', handle_balloon_update)

API Reference ​

Configuration Options ​

python
await UrbanSkySDK.init({
    'apiToken': 'your-api-token',  # Required - Your API token
    'baseUrl': 'custom-api-url'    # Optional - Custom API URL (rarely needed)
})

Note: Most configuration options have sensible defaults. You typically only need to provide your API token.

Methods ​

Note: The init() method automatically handles both initialization and connection, so you typically don't need to call connect() separately.

async disconnect() -> None ​

Closes the connection to Urban Sky. This is a coroutine — it must be awaited.

python
await sdk.disconnect()

on(event: str, callback: Callable) -> None ​

Registers an event listener. Every handler receives exactly one payload argument, so even handlers that ignore the payload must accept it.

python
sdk.on('balloon:update', handle_balloon_update)
sdk.on('connected', lambda _: print('SDK connected'))
sdk.on('disconnected', lambda _: print('SDK disconnected'))
sdk.on('error', lambda error: print(f'SDK error: {error}'))

off(event: str, callback: Callable = None) -> None ​

Removes event listener(s).

python
# Remove specific listener
sdk.off('balloon:update', handle_balloon_update)

# Remove all listeners for event
sdk.off('balloon:update')

Events ​

balloon:update ​

Emitted when balloon position data is updated.

Payload: a BalloonUpdate dataclass. Attributes are snake_case (the SDK converts the camelCase wire format for you before your handler sees it):

python
update.balloon_id     # str
update.mission_id     # str
update.position       # BalloonPosition or None - where the balloon is (see below)
update.devices        # list of device dataclasses:

device.device_id      # str
device.device_type    # str - e.g., "PLD" (Payload), "APX" (Apex), "BLS" (Ballaster)
device.lat            # float - Latitude
device.lng            # float - Longitude
device.altitude       # float - Altitude in meters (when available)
device.timestamp      # str - ISO 8601 timestamp

update.position is the balloon's location, the same one the Urban Sky map shows. Use it instead of picking a device yourself. It comes from the Apex while the Apex reports, then from a Husky Bus or cutdown Apex flying with it, and from a lower-accuracy GPS (RAD, BAL) only after 15 minutes without a reliable fix. It is None until a device reports a fix.

python
position.device_id       # str - the device the fix came from
position.device_type     # str - e.g., "APX", "USK", "RAD"
position.lat             # float
position.lng             # float
position.altitude        # float or None - meters
position.timestamp       # str - ISO 8601 timestamp of the fix
position.lower_accuracy  # bool - True when the fix is from RAD/BAL

connected ​

Emitted when successfully connected to Urban Sky.

disconnected ​

Emitted when disconnected from Urban Sky.

error ​

Emitted when an error occurs.

Payload: a dict with two keys:

python
{
    'message': str,  # Human-readable description of what failed
    'error': str     # Error detail or code, e.g. 'RECONNECT_FAILED'
}

Complete Example ​

python
# example.py
import asyncio
import os

import requests

# Load the SDK dynamically
exec(requests.get('https://sdk.atmosys.com/runtime/py/current/loader.py').text)

async def main():
    try:
        # Initialize and connect SDK
        sdk = await UrbanSkySDK.init({
            'apiToken': os.getenv('URBAN_SKY_API_TOKEN')
        })

        # Connection events
        sdk.on('connected', lambda _: print('✅ Connected to Urban Sky'))
        sdk.on('disconnected', lambda _: print('❌ Disconnected'))
        sdk.on('error', lambda error: print(f'❌ Error: {error}'))

        # Balloon updates
        def handle_balloon_update(update):
            print(f'🎈 Balloon {update.balloon_id} update:')
            print(f'   Mission: {update.mission_id}')

            for device in update.devices:
                print(f'   📍 Device {device.device_id}: {device.lat}, {device.lng}')
                print(f'   🕒 Time: {device.timestamp}')

        sdk.on('balloon:update', handle_balloon_update)

        print('SDK initialized and connected!')

        # Keep the script running
        try:
            while True:
                await asyncio.sleep(1)
        except KeyboardInterrupt:
            print('Disconnecting...')
            await sdk.disconnect()

    except Exception as error:
        print(f'Failed to initialize SDK: {error}')

if __name__ == '__main__':
    asyncio.run(main())

Testing Your Implementation ​

You can test your SDK integration using our test endpoint:

python
import requests

# Send a test balloon update to verify your implementation
def send_test_update(api_token):
    url = 'https://api.ops.atmosys.com/sdk/test/balloon'
    headers = {
        'x-api-token': api_token,
        'Content-Type': 'application/json'
    }

    try:
        response = requests.post(url, headers=headers, json={})
        if response.status_code == 200:
            print('Test message sent - you should receive it via your SDK listener')
        else:
            print(f'Test failed with status: {response.status_code}')
    except Exception as e:
        print(f'Test request failed: {e}')

# Usage
send_test_update('your-api-token')

Virtual Environment Setup ​

It's recommended to use a virtual environment:

bash
# Create virtual environment
python -m venv venv

# Activate (Linux/Mac)
source venv/bin/activate

# Activate (Windows)
venv\Scripts\activate

# Install dependencies
pip install requests centrifuge-python

# Run your script
python example.py

Dependency Installation Issues ​

Common Issues ​

"No module named 'requests'" / "No module named 'centrifuge'"

requests is needed to fetch the loader, and centrifuge-python>=0.3.0 is a required dependency of the SDK — both must be available in your environment.

bash
# Install the loader and SDK dependencies
pip install requests centrifuge-python

# Or using a virtual environment (recommended)
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install requests centrifuge-python

"ModuleNotFoundError" with async/await

  • Ensure you're using Python 3.8+ for proper async support

Error Handling ​

Common Error Codes ​

  • AUTH_INVALID_TOKEN - API token is invalid or expired
  • AUTH_INSUFFICIENT_PERMISSIONS - Token lacks SDK permissions
  • CONNECTION_FAILED - Network connection issues
  • SERVICE_UNAVAILABLE - Urban Sky service temporarily unavailable

Best Practices ​

python
import asyncio
import os

import requests

# Load the SDK dynamically
exec(requests.get('https://sdk.atmosys.com/runtime/py/current/loader.py').text)

async def main():
    # Handle connection errors
    def handle_error(error):
        if isinstance(error, dict):
            error_code = error.get('error', 'UNKNOWN')
        else:
            error_code = 'UNKNOWN'

        if error_code == 'AUTH_INVALID_TOKEN':
            print('Invalid API token - check your credentials')
        elif error_code == 'CONNECTION_FAILED':
            print('Connection failed - retrying...')
            # SDK will automatically retry
        else:
            print(f'SDK error: {error}')

    # Initialize and connect with error handling
    try:
        sdk = await UrbanSkySDK.init({
            'apiToken': os.getenv('URBAN_SKY_API_TOKEN')
        })

        sdk.on('error', handle_error)

        # Handle reconnection
        sdk.on('disconnected', lambda _: print('Disconnected - will attempt to reconnect'))
        sdk.on('connected', lambda _: print('Connected/reconnected successfully'))

    except Exception as error:
        print(f'Initial connection failed: {error}')
        # Handle startup failure
        return

    # Your application logic here
    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        await sdk.disconnect()

if __name__ == '__main__':
    asyncio.run(main())

Payload logs and metrics ​

The Python SDK also has PayloadTelemetry, a client that runs on a payload and sends logs and metrics to its Husky bus. It is separate from UrbanSkySDK: it needs no API token, and you copy it into your payload software as one file. See the Python reference and Send payload logs and metrics.

Next Steps ​