This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 8cd83abb6 chore(example): test python examples in CI (#2174)
8cd83abb6 is described below
commit 8cd83abb6cbb0f6cb5da157c38be714a848782d5
Author: Huan-Cheng Chang <[email protected]>
AuthorDate: Tue Nov 18 12:12:08 2025 +0000
chore(example): test python examples in CI (#2174)
* Added one script `scripts/run-python-examples-from-readme.sh` which
runs all python examples and returns an error when any of the examples
fails. For consistency, the script basically aligns with other scripts
for different SDKs.
* Added one github action that builds the python SDK package and runs
example tests. Since checking python examples requires building and
installing the SDK, which means a few more setup steps, the check is
turned into a github action.
* Added one task that checks python examples using the new action above.
* Updated a few things regarding the examples, e.g. fixing the default
argument in the basic example, so that they work with the test script.
---------
Co-authored-by: Hubert Gruszecki <[email protected]>
---
.github/config/components.yml | 4 +-
.github/workflows/_test_examples.yml | 22 +++-
examples/python/README.md | 6 +-
examples/python/basic/consumer.py | 7 +-
examples/python/basic/producer.py | 9 +-
examples/python/getting-started/consumer.py | 6 +-
examples/python/getting-started/producer.py | 8 +-
examples/python/requirements.txt | 2 +-
scripts/run-python-examples-from-readme.sh | 168 ++++++++++++++++++++++++++++
9 files changed, 210 insertions(+), 22 deletions(-)
diff --git a/.github/config/components.yml b/.github/config/components.yml
index 1d775d85f..e77b3d9b7 100644
--- a/.github/config/components.yml
+++ b/.github/config/components.yml
@@ -237,13 +237,15 @@ components:
- "rust-server"
- "sdk-go"
- "sdk-csharp"
+ - "sdk-python"
- "ci-infrastructure" # CI changes trigger full regression
paths:
- "examples/**"
- "scripts/run-rust-examples-from-readme.sh"
- "scripts/run-go-examples-from-readme.sh"
- "scripts/run-csharp-examples-from-readme.sh"
- tasks: ["examples-rust", "examples-go", "examples-csharp"]
+ - "scripts/run-python-examples-from-readme.sh"
+ tasks: ["examples-rust", "examples-go", "examples-csharp",
"examples-python"]
web-ui:
paths:
diff --git a/.github/workflows/_test_examples.yml
b/.github/workflows/_test_examples.yml
index 89e847141..38e03a83a 100644
--- a/.github/workflows/_test_examples.yml
+++ b/.github/workflows/_test_examples.yml
@@ -52,6 +52,19 @@ jobs:
with:
cache-targets: false # Only cache registry and git deps, not target
dir (sccache handles that)
+ - name: Setup Python
+ if: inputs.component == 'examples-suite'
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Cache pip
+ if: inputs.component == 'examples-suite'
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: pip-${{ runner.os }}-${{
hashFiles('foreign/python/pyproject.toml') }}
+
- name: Build common binaries for all examples
if: inputs.component == 'examples-suite'
run: |
@@ -95,23 +108,26 @@ jobs:
if: inputs.component == 'examples-suite' && inputs.task ==
'examples-rust'
run: |
echo "Running Rust examples tests..."
- # Run the examples script which will use the prebuilt binaries
./scripts/run-rust-examples-from-readme.sh
- name: Run Go examples
if: inputs.component == 'examples-suite' && inputs.task ==
'examples-go'
run: |
echo "Running Go examples tests..."
- # Run the examples script which will use the prebuilt server binary
./scripts/run-go-examples-from-readme.sh
- name: Run Csharp examples
if: inputs.component == 'examples-suite' && inputs.task ==
'examples-csharp'
run: |
echo "Running Csharp examples tests..."
- # Run the examples script which will use the prebuilt server binary
./scripts/run-csharp-examples-from-readme.sh
+ - name: Run Python examples
+ if: inputs.component == 'examples-suite' && inputs.task ==
'examples-python'
+ run: |
+ echo "Running Python examples tests..."
+ ./scripts/run-python-examples-from-readme.sh
+
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
diff --git a/examples/python/README.md b/examples/python/README.md
index eab95b923..1bcee58eb 100644
--- a/examples/python/README.md
+++ b/examples/python/README.md
@@ -30,7 +30,7 @@ IGGY_HTTP_ENABLED=true IGGY_TCP_ADDRESS=0.0.0.0:8090 cargo
run --bin iggy-server
and then install Python dependencies:
```bash
-pip -r requirements.txt
+pip install -r requirements.txt
```
## Basic Examples
@@ -49,8 +49,8 @@ python getting-started/consumer.py
Core functionality with detailed configuration options:
```bash
-python basic/producer.py <connection_string>
-python basic/consumer.py <connection_string>
+python basic/producer.py
+python basic/consumer.py
```
Demonstrates fundamental client connection, authentication, batch message
sending, and polling with support for TCP/QUIC/HTTP protocols.
diff --git a/examples/python/basic/consumer.py
b/examples/python/basic/consumer.py
index 8523bf8cf..2097f98de 100644
--- a/examples/python/basic/consumer.py
+++ b/examples/python/basic/consumer.py
@@ -24,9 +24,9 @@ from loguru import logger
STREAM_NAME = "sample-stream"
TOPIC_NAME = "sample-topic"
-STREAM_ID = 1
-TOPIC_ID = 1
-PARTITION_ID = 1
+STREAM_ID = 0
+TOPIC_ID = 0
+PARTITION_ID = 0
BATCHES_LIMIT = 5
ArgNamespace = namedtuple("ArgNamespace", ["connection_string"])
@@ -40,6 +40,7 @@ def parse_args() -> argparse.Namespace:
"Connection string for Iggy client, e.g.
'iggy+tcp://iggy:[email protected]:8090'"
),
default="iggy+tcp://iggy:[email protected]:8090",
+ nargs="?",
type=str,
)
return parser.parse_args()
diff --git a/examples/python/basic/producer.py
b/examples/python/basic/producer.py
index b3548f70d..e2c6d524b 100644
--- a/examples/python/basic/producer.py
+++ b/examples/python/basic/producer.py
@@ -25,9 +25,9 @@ from loguru import logger
STREAM_NAME = "sample-stream"
TOPIC_NAME = "sample-topic"
-STREAM_ID = 1
-TOPIC_ID = 1
-PARTITION_ID = 1
+STREAM_ID = 0
+TOPIC_ID = 0
+PARTITION_ID = 0
BATCHES_LIMIT = 5
ArgNamespace = namedtuple("ArgNamespace", ["connection_string"])
@@ -41,6 +41,7 @@ def parse_args() -> argparse.Namespace:
"Connection string for Iggy client, e.g.
'iggy+tcp://iggy:[email protected]:8090'"
),
default="iggy+tcp://iggy:[email protected]:8090",
+ nargs="?",
type=str,
)
return parser.parse_args()
@@ -61,7 +62,7 @@ async def init_system(client: IggyClient):
logger.info(f"Creating stream with name {STREAM_NAME}...")
stream: StreamDetails = await client.get_stream(STREAM_NAME)
if stream is None:
- await client.create_stream(name=STREAM_NAME, stream_id=STREAM_ID)
+ await client.create_stream(name=STREAM_NAME)
logger.info("Stream was created successfully.")
else:
logger.warning(f"Stream {stream.name} already exists with ID
{stream.id}")
diff --git a/examples/python/getting-started/consumer.py
b/examples/python/getting-started/consumer.py
index 37cb51e69..30a78d5ba 100644
--- a/examples/python/getting-started/consumer.py
+++ b/examples/python/getting-started/consumer.py
@@ -27,9 +27,9 @@ from loguru import logger
STREAM_NAME = "sample-stream"
TOPIC_NAME = "sample-topic"
-STREAM_ID = 1
-TOPIC_ID = 1
-PARTITION_ID = 1
+STREAM_ID = 0
+TOPIC_ID = 0
+PARTITION_ID = 0
BATCHES_LIMIT = 5
ArgNamespace = namedtuple("ArgNamespace", ["tcp_server_address"])
diff --git a/examples/python/getting-started/producer.py
b/examples/python/getting-started/producer.py
index 71ccb2232..36445bfac 100644
--- a/examples/python/getting-started/producer.py
+++ b/examples/python/getting-started/producer.py
@@ -28,9 +28,9 @@ from loguru import logger
STREAM_NAME = "sample-stream"
TOPIC_NAME = "sample-topic"
-STREAM_ID = 1
-TOPIC_ID = 1
-PARTITION_ID = 1
+STREAM_ID = 0
+TOPIC_ID = 0
+PARTITION_ID = 0
BATCHES_LIMIT = 5
ArgNamespace = namedtuple("ArgNamespace", ["tcp_server_address"])
@@ -78,7 +78,7 @@ async def init_system(client: IggyClient):
logger.info(f"Creating stream with name {STREAM_NAME}...")
stream: StreamDetails = await client.get_stream(STREAM_NAME)
if stream is None:
- await client.create_stream(name=STREAM_NAME, stream_id=STREAM_ID)
+ await client.create_stream(name=STREAM_NAME)
logger.info("Stream was created successfully.")
else:
logger.warning(f"Stream {stream.name} already exists with ID
{stream.id}")
diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt
index 11231bec6..e46e8562d 100644
--- a/examples/python/requirements.txt
+++ b/examples/python/requirements.txt
@@ -15,6 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-apache-iggy==0.5.0
+apache-iggy
loguru==0.7.3
argparse==1.4.0
diff --git a/scripts/run-python-examples-from-readme.sh
b/scripts/run-python-examples-from-readme.sh
new file mode 100755
index 000000000..43abb0648
--- /dev/null
+++ b/scripts/run-python-examples-from-readme.sh
@@ -0,0 +1,168 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -euo pipefail
+
+# Script to run Python examples from README.md and examples/python/README.md
files
+# Usage: ./scripts/run-python-examples-from-readme.sh [TARGET]
+#
+# TARGET - Optional target architecture (e.g., x86_64-unknown-linux-musl)
+# If not provided, uses the default target
+#
+# This script will run all the commands from both README.md and
examples/python/README.md files
+# and check if they pass or fail.
+# If any command fails, it will print the command and exit with non-zero
status.
+# If all commands pass, it will remove the log file and exit with zero status.
+#
+# Note: This script assumes that the iggy-server is not running and will start
it in the background.
+# It will wait until the server is started before running the commands.
+# It will also terminate the server after running all the commands.
+# Script executes every command in README files which is enclosed in
backticks (`) and starts
+# with `python `. Other commands are ignored.
+# Order of commands in README files is important as script will execute
them from top to bottom.
+
+readonly LOG_FILE="iggy-server.log"
+readonly PID_FILE="iggy-server.pid"
+readonly TIMEOUT=300
+
+TARGET="${1:-}" # Iggy server target architecture
+
+if [ -n "${TARGET}" ]; then
+ echo "Using target architecture: ${TARGET}"
+else
+ echo "Using default target architecture"
+fi
+
+# Remove old server data if present
+test -d local_data && rm -fr local_data
+test -e ${LOG_FILE} && rm ${LOG_FILE}
+test -e ${PID_FILE} && rm ${PID_FILE}
+
+# Check if server binary exists
+SERVER_BIN=""
+if [ -n "${TARGET}" ]; then
+ SERVER_BIN="target/${TARGET}/debug/iggy-server"
+else
+ SERVER_BIN="target/debug/iggy-server"
+fi
+
+if [ ! -f "${SERVER_BIN}" ]; then
+ echo "Error: Server binary not found at ${SERVER_BIN}"
+ echo "Please build the server binary before running this script:"
+ if [ -n "${TARGET}" ]; then
+ echo " cargo build --target ${TARGET} --bin iggy-server"
+ else
+ echo " cargo build --bin iggy-server"
+ fi
+ exit 1
+fi
+
+echo "Using server binary at ${SERVER_BIN}"
+
+# Run iggy server using the prebuilt binary
+echo "Starting server from ${SERVER_BIN}..."
+IGGY_ROOT_USERNAME=iggy IGGY_ROOT_PASSWORD=iggy ${SERVER_BIN} --fresh
&>${LOG_FILE} &
+echo $! >${PID_FILE}
+
+# Wait until "Iggy server has started" string is present inside iggy-server.log
+SERVER_START_TIME=0
+while ! grep -q "has started" ${LOG_FILE}; do
+ if [ ${SERVER_START_TIME} -gt ${TIMEOUT} ]; then
+ echo "Server did not start within ${TIMEOUT} seconds."
+ ps fx
+ cat ${LOG_FILE}
+ exit 1
+ fi
+ echo "Waiting for Iggy server to start... ${SERVER_START_TIME}"
+ sleep 1
+ ((SERVER_START_TIME += 1))
+done
+
+cd examples/python || exit 1
+
+# Set up Python virtual environment and install dependencies
+echo "Setting up Python virtual environment..."
+python3 -m venv .venv
+# shellcheck disable=SC1091
+source .venv/bin/activate
+
+# Build and install Python SDK from repository
+echo "Building Python SDK from repository..."
+pip install -q maturin patchelf
+(cd ../../foreign/python && maturin develop -q)
+
+# Install other example dependencies (excluding apache-iggy which we built
locally)
+echo "Installing example dependencies..."
+pip install -q loguru argparse
+
+# Execute all example commands from examples/python/README.md and check if
they pass or fail
+exit_code=0
+if [ -f "README.md" ]; then
+ while IFS= read -r command; do
+ # Remove backticks and comments from command
+ command=$(echo "${command}" | tr -d '`' | sed 's/^#.*//')
+ # Skip empty lines
+ if [ -z "${command}" ]; then
+ continue
+ fi
+
+ echo -e "\e[33mChecking example command from
examples/python/README.md:\e[0m ${command}"
+ echo ""
+
+ set +e
+ eval "timeout 10 ${command}"
+ test_exit_code=$?
+ set -e
+
+ # Stop at first failure
+ # Since examples might last longer, timeout error 124 is ignored here
+ if [[ $test_exit_code -ne 0 && $test_exit_code -ne 124 ]]; then
+ echo ""
+ echo -e "\e[31mExample command failed:\e[0m ${command}"
+ echo ""
+ exit_code=$test_exit_code
+ break
+ fi
+ # Add a small delay between examples to avoid potential race conditions
+ sleep 2
+
+ done < <(grep -E "^python " "README.md")
+fi
+
+# Deactivate and clean up virtual environment
+deactivate 2>/dev/null || true
+rm -rf .venv
+
+cd ../..
+
+# Terminate server
+kill -TERM "$(cat ${PID_FILE})"
+test -e ${PID_FILE} && rm ${PID_FILE}
+
+# If everything is ok remove log and pid files otherwise cat server log
+if [ "${exit_code}" -eq 0 ]; then
+ echo "Test passed"
+else
+ echo "Test failed, see log file:"
+ test -e ${LOG_FILE} && cat ${LOG_FILE}
+fi
+
+test -e ${LOG_FILE} && rm ${LOG_FILE}
+test -e ${PID_FILE} && rm ${PID_FILE}
+
+exit "${exit_code}"