Skip to main content

A modern, intuitive Python framework for building USSD applications with FastAPI

Project description

Cardisk — USSD Framework for Python

A modern, intuitive Python framework for building USSD applications with FastAPI. Cardisk provides a clean, decorator-based API for creating interactive USSD menus and flows.

Features

  • Simple API - Decorator-based routing similar to Flask/FastAPI
  • Menu System - Built-in support for interactive menus with automatic navigation
  • Session Management - Automatic session handling with in-memory or Redis storage
  • Monitoring - Built-in Prometheus metrics endpoint
  • Error Handling - Robust error handling middleware
  • Type Safe - Full type hints support
  • Fast - Built on FastAPI and Uvicorn

Installation

Development Install

pip install -e .

Production Install

pip install cardisk

Quick Start

Create a file app.py:

from cardisk import Cardisk, ussd

app = Cardisk()

@app.route("/")
def main_menu():
    ussd.menu("Welcome to Wolke Services", [
        ("Loans", "/loans"),
        ("Savings", "/savings"),
        ("Insurance", "/insurance")
    ])

@app.route("/loans")
def loans():
    ussd.menu("Loans Menu", [
        ("Apply for loan", "/loan/apply"),
        ("Check loan status", "/loan/status"),
        ("Repay loan", "/loan/repay")
    ])

@app.route("/loan/apply")
def loan_apply():
    ussd.screen(f"Enter loan amount for {ussd.msisdn}")
    ussd.next("/loan/confirm")

@app.route("/loan/confirm")
def loan_confirm():
    amount = ussd.input
    ussd.screen(f"Loan of ${amount} submitted successfully for {ussd.msisdn}")
    ussd.end()

Run the application:

cardisk runserver app.py

Or with uvicorn directly:

uvicorn app:app.app --reload --port 8000

API Reference

Application Setup

from cardisk import Cardisk, ussd

app = Cardisk()

Route Decorator

Define routes using the @app.route() decorator:

@app.route("/path")
def handler():
    # Your logic here
    pass
  • Routes should start with /
  • Use / for the main menu (entry point)

USSD Object

The ussd object provides access to request context and response methods:

Properties

  • ussd.msisdn - User's phone number (str)
  • ussd.session - Current session ID (str)
  • ussd.input - User's input from the previous screen (str)

Methods

ussd.menu(title, options)

Display a numbered menu to the user:

ussd.menu("Main Menu", [
    ("Option 1", "/route1"),
    ("Option 2", "/route2"),
    ("Option 3", "/route3")
])
  • title (str): Menu heading
  • options (List[Tuple[str, str]]): List of (label, route) tuples

The menu automatically handles user input (1, 2, 3, etc.) and routes to the corresponding path.

ussd.screen(text)

Display a text screen:

ussd.screen(f"Hello {ussd.msisdn}!")
ussd.next(route)

Set the next route to navigate to after user input:

ussd.screen("Enter your PIN:")
ussd.next("/verify-pin")
ussd.end()

End the USSD session:

ussd.screen("Thank you!")
ussd.end()

USSD XML Protocol

Cardisk uses XML for USSD communication:

Request Format

<ussd>
    <type>1</type>  <!-- 1=new session, 2=continue -->
    <msisdn>254712345678</msisdn>
    <sessionid>unique-session-id</sessionid>
    <msg>user-input</msg>
</ussd>

Response Format

<ussd>
    <type>2</type>  <!-- 2=continue, 3=end -->
    <msg>Response text</msg>
</ussd>

Testing Your USSD App

Using cURL

1. Start a new session:

curl -X POST http://localhost:8000/ \
  -H "Content-Type: application/xml" \
  -d '<ussd><type>1</type><msisdn>254712345678</msisdn><sessionid>test123</sessionid><msg></msg></ussd>'

2. Select a menu option (e.g., option 1):

curl -X POST http://localhost:8000/ \
  -H "Content-Type: application/xml" \
  -d '<ussd><type>2</type><msisdn>254712345678</msisdn><sessionid>test123</sessionid><msg>1</msg></ussd>'

3. Enter input:

curl -X POST http://localhost:8000/ \
  -H "Content-Type: application/xml" \
  -d '<ussd><type>2</type><msisdn>254712345678</msisdn><sessionid>test123</sessionid><msg>500</msg></ussd>'

Using Python

import httpx

client = httpx.Client(base_url="http://localhost:8000")

# Start session
response = client.post("/", content="""
<ussd>
    <type>1</type>
    <msisdn>254712345678</msisdn>
    <sessionid>test123</sessionid>
    <msg></msg>
</ussd>
""", headers={"Content-Type": "application/xml"})

print(response.text)

# Continue session
response = client.post("/", content="""
<ussd>
    <type>2</type>
    <msisdn>254712345678</msisdn>
    <sessionid>test123</sessionid>
    <msg>1</msg>
</ussd>
""", headers={"Content-Type": "application/xml"})

print(response.text)

Example Application

Check out the full example in examples/mybank/main.py:

cd examples/mybank
cardisk runserver main.py

Or:

cd examples/mybank
uvicorn main:app.app --reload

Session Management

In-Memory Sessions (Default)

By default, Cardisk uses in-memory session storage, suitable for development and single-instance deployments.

Redis Sessions

For production with multiple instances, use Redis:

export CARDISK_USE_REDIS=true
export CARDISK_REDIS_URL=redis://localhost:6379/0

cardisk runserver app.py

Monitoring

Prometheus Metrics

Cardisk exposes metrics at /metrics:

curl http://localhost:8000/metrics

Available metrics:

  • cardisk_requests_total - Total USSD requests by type

Docker

Build

docker build -t cardisk-app .

Run

docker run -p 8000:8000 cardisk-app

With Redis

docker run -p 8000:8000 \
  -e CARDISK_USE_REDIS=true \
  -e CARDISK_REDIS_URL=redis://redis:6379/0 \
  cardisk-app

CLI Commands

Create a new project

cardisk new myproject

Run the server

cardisk runserver app.py --host 0.0.0.0 --port 8000

Advanced Usage

Custom Error Handling

@app.route("/transfer")
def transfer():
    try:
        amount = float(ussd.input)
        if amount <= 0:
            ussd.screen("Invalid amount. Please try again.")
            ussd.next("/transfer")
        else:
            # Process transfer
            ussd.screen(f"Transfer of ${amount} successful!")
            ussd.end()
    except ValueError:
        ussd.screen("Invalid input. Please enter a number.")
        ussd.next("/transfer")

Multi-Step Forms

@app.route("/register")
def register_name():
    ussd.screen("Enter your name:")
    ussd.next("/register/age")

@app.route("/register/age")
def register_age():
    name = ussd.input
    ussd.screen(f"Hello {name}! Enter your age:")
    ussd.next("/register/confirm")

@app.route("/register/confirm")
def register_confirm():
    age = ussd.input
    ussd.screen(f"Registration complete! Age: {age}")
    ussd.end()

Testing

Run Tests

pytest

Run with Coverage

pytest --cov=cardisk --cov-report=html

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License

Support

For issues and questions, please open an issue on GitHub.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cardisk-0.4.7.tar.gz (19.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cardisk-0.4.7-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file cardisk-0.4.7.tar.gz.

File metadata

  • Download URL: cardisk-0.4.7.tar.gz
  • Upload date:
  • Size: 19.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for cardisk-0.4.7.tar.gz
Algorithm Hash digest
SHA256 d6d82dee7277ecf94ca0d5b10c38c8a7e16568ee6795213e906b9c040d5e1471
MD5 7cfe7158a6033775246316d6dfc37fd7
BLAKE2b-256 2186e1d264ef2cfb7d4754c8b2ee65c2c43638787c9cb589d7d01a7fe481eee4

See more details on using hashes here.

File details

Details for the file cardisk-0.4.7-py3-none-any.whl.

File metadata

  • Download URL: cardisk-0.4.7-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for cardisk-0.4.7-py3-none-any.whl
Algorithm Hash digest
SHA256 6bef572765e628fd1d72f21c89796a4552efa22edd114125021979ce7d3cfcc4
MD5 179ccba3b5f0f5a6b0f609669634e45e
BLAKE2b-256 16810fa1f85638fd63abcbb95a9df8f7561c39e694862fd5ba59dab20f0331b8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page