AI/LLM-friendly Python client for SAP OData V2 and V4 APIs - single generic function for all operations
Project description
SAP OData Python
Simple, AI/LLM-friendly Python client for SAP OData V2 and V4 services.
Features
- SAP-First Design - Built specifically for SAP OData services (V2 Gateway & V4 RAP)
- AI/LLM Friendly - Single generic function design, perfect for AI agents
- Simple API - One client, one method signature for all operations
- Automatic URL Building - Handles complex SAP URL patterns automatically
- Input Validation - Clear error messages for missing parameters
- Raw Responses - Returns actual API response with helper methods to extract data
Installation
pip install sap-odata-python
Quick Start
from sap_odata import ODataClient
# Connect to SAP system (sap_mode=True is default)
client = ODataClient(
"https://sap-system.company.com:44300",
username="your_user",
password="your_password",
client="100"
)
# SAP OData V4 (RAP Services)
response = client.get(
service="zsd_my_service",
entity="MyEntity",
version="v4",
namespace="zsb_my_service",
top=10
)
# SAP OData V2 (Gateway Services)
response = client.get(
service="ZMY_SALESORDER_SRV",
entity="SalesOrderSet",
version="v2",
top=10
)
# Access data directly
for item in response["d"]: # V2 format
print(item)
SAP OData V4 Examples
# Simple query with filter
response = client.get(
service="zsd_product_api",
entity="Products",
version="v4",
namespace="zsb_product_api",
filter="ProductID eq '12345'",
select="ProductID,Name,Price"
)
# Complex nested $expand
response = client.get(
service="zsd_order_api",
entity="Orders",
version="v4",
namespace="zsb_order_api",
filter="OrderID eq '12345'",
expand="Customer,LineItems($expand=Product)"
)
# Access nested data
for order in response["value"]: # V4 format
print(f"Order: {order['OrderID']}")
for item in order.get("LineItems", []):
print(f" Item: {item['ProductName']}")
# Count only (no data returned) - calls /Entity/$count
total_products = client.count(
service="zsd_product_api",
entity="Products",
version="v4",
namespace="zsb_product_api"
)
print(f"Total products: {total_products}") # 245
# Count with filter
open_orders = client.count(
service="zsd_order_api",
entity="Orders",
version="v4",
namespace="zsb_order_api",
filter="Status eq 'OPEN'"
)
print(f"Open orders: {open_orders}") # 42
# Inline count (data + total count)
response = client.get(
service="zsd_product_api",
entity="Products",
version="v4",
namespace="zsb_product_api",
top=10,
count=True # Adds $count=true
)
total = client.get_count(response, "v4") # from @odata.count
items = client.get_value(response, "v4") # from value[]
print(f"Showing {len(items)} of {total} products")
SAP OData V2 Examples
# Query with filter and select
response = client.get(
service="ZMY_SALESORDER_SRV",
entity="SalesOrderSet",
version="v2",
filter="Status eq 'OPEN'",
select="OrderID,CustomerID,Amount",
top=100
)
# Count only (V2)
total = client.count(
service="ZMY_SALESORDER_SRV",
entity="SalesOrderSet",
version="v2"
)
print(f"Total orders: {total}")
# Inline count (V2)
response = client.get(
service="ZMY_SALESORDER_SRV",
entity="SalesOrderSet",
version="v2",
top=20,
count=True
)
# V2 response: {"d": {"__count": "245", "results": [...]}}
total = client.get_count(response, "v2") # from d.__count
items = client.get_value(response, "v2") # from d.results or d[]
# Entity with key in path
response = client.get(
service="ZMY_CUSTOMER_SRV",
entity="CustomerSet('CUST001')",
version="v2"
)
# Complex nested $expand (V2 uses / for nested)
response = client.get(
service="ZMY_ORDER_SRV",
entity="OrderSet",
version="v2",
expand="OrderToCustomer,OrderToItems/ItemToProduct"
)
# V2 nested results use "results" arrays
for order in response["d"]: # or response["d"]["results"] depending on service
for item in order.get("OrderToItems", {}).get("results", []):
print(f" Item: {item['ProductName']}")
SAP URL Patterns (Handled Automatically)
| Version | URL Pattern |
|---|---|
| V4 | /sap/opu/odata4/sap/{namespace}/srvd_a2x/sap/{service}/0001/{entity} |
| V2 | /sap/opu/odata/sap/{service}/{entity} |
You just provide service, entity, and namespace (for V4) - the library builds the full URL.
API Reference
Constructor
client = ODataClient(
host, # SAP system URL
username="", # SAP username
password="", # SAP password
client="", # SAP client number (e.g., "100")
sap_mode=True, # True for SAP, False for other OData services
verify_ssl=True, # SSL verification
timeout=60 # Request timeout
)
Methods
| Method | Description |
|---|---|
get(service, entity, version, namespace, **params) |
Read data (GET) |
post(service, entity, data, version, namespace) |
Create record (POST) |
put(service, entity, data, version, namespace) |
Replace record (PUT) |
patch(service, entity, data, version, namespace) |
Update record (PATCH) |
delete(service, entity, version, namespace) |
Delete record (DELETE) |
count(service, entity, version, namespace, **params) |
Get count only (no data) |
metadata(service, version, namespace) |
Get service metadata (XML) |
get_value(response, version) |
Extract data list from response |
get_count(response, version) |
Extract inline count from response |
get_next_link(response, version) |
Extract pagination URL |
Query Parameters
| Parameter | Example | Description |
|---|---|---|
top |
top=10 |
Limit results |
skip |
skip=20 |
Skip records |
filter |
filter="Price gt 100" |
Filter expression |
select |
select="ID,Name" |
Select fields |
expand |
expand="Items" |
Expand navigation |
orderby |
orderby="Name asc" |
Sort results |
count |
count=True |
Include inline count |
search |
search="keyword" |
Free text search |
Counting Records
Two ways to get counts:
1. Count Only (no data) - /Entity/$count
# Count all products
total = client.count("zsd_product_api", "Products", version="v4", namespace="zsb_product_api")
print(total) # 245
# Count with filter
open_orders = client.count("ZMY_SRV", "Orders", version="v2", filter="Status eq 'OPEN'")
print(open_orders) # 42
2. Inline Count (with data) - $count=true
# Get data with total count
response = client.get("zsd_product_api", "Products", version="v4", namespace="zsb_product_api",
top=10, count=True)
# V4: {"@odata.count": 245, "value": [...]}
total = client.get_count(response, "v4") # 245
items = client.get_value(response, "v4") # [...]
# V2: {"d": {"__count": "245", "results": [...]}}
total = client.get_count(response, "v2") # 245
Response Format
Responses are returned raw (as received from the API):
# V4 response
{"@odata.context": "...", "value": [...], "@odata.nextLink": "...", "@odata.count": 100}
# V2 response
{"d": [...]} # or {"d": {"results": [...], "__next": "...", "__count": "100"}}
Helper Methods
Use helper methods to extract data from responses:
response = client.get("service", "Products", top=10, count=True)
# Extract data list
items = client.get_value(response, "v4") # Returns list from "value" or "d"
# Get total count (if $count was requested)
total = client.get_count(response, "v4") # Returns @odata.count or -1
# Get next page URL
next_url = client.get_next_link(response, "v4") # from @odata.nextLink
next_url = client.get_next_link(response, "v2") # from d.__next
Non-SAP OData Services
For public services like Northwind, set sap_mode=False:
client = ODataClient("https://services.odata.org", sap_mode=False)
# Northwind V4
data = client.get("V4/Northwind/Northwind.svc", "Products", top=5)
# Northwind V2
data = client.get("V2/Northwind/Northwind.svc", "Products", version="v2", top=5)
Error Handling
from sap_odata import ODataClient, ODataError, ODataConnectionError, ODataAuthError
try:
data = client.get("zsd_my_service", "MyEntity", version="v4", namespace="zsb_my_service")
except ODataAuthError:
print("Authentication failed")
except ODataConnectionError as e:
print(f"Connection failed: {e}")
except ODataError as e:
print(f"OData error: {e}")
Context Manager
with ODataClient("https://sap.company.com", "user", "pass", client="100") as client:
data = client.get("ZMY_SRV", "OrderSet", version="v2")
# Session automatically closed
License
Apache 2.0
Links
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sap_odata_python-1.2.0.tar.gz.
File metadata
- Download URL: sap_odata_python-1.2.0.tar.gz
- Upload date:
- Size: 23.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd83b043934c927d07196057e350bff7a8820357b17ea9196aecf21bba71e48c
|
|
| MD5 |
136681df18b5b471ae7152b6b5e2abb6
|
|
| BLAKE2b-256 |
7345ed3f0b33d6eaa0b941225d5bab43e5152e94bb3429abe19ad47d647b713f
|
File details
Details for the file sap_odata_python-1.2.0-py3-none-any.whl.
File metadata
- Download URL: sap_odata_python-1.2.0-py3-none-any.whl
- Upload date:
- Size: 12.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5947e7323a2fa2b8596333bbde1c59a4f55d78c5289aaed54aeabaf18a4ec521
|
|
| MD5 |
c7ae8ddc3edb7176b41cb6f05897f7a8
|
|
| BLAKE2b-256 |
fd3f9f5d2c57c7d9cf77abb34d854a1183a9bb0744ed44c4caeae36f33bf58b6
|