Paginated OData Fetch Loop (Python Generator)
Pulling a large entity from an ERP over OData in one call usually fails or times out. This generator writes a Python script that requests a page, follows the server's next link until the data ends, and writes rows to CSV with a cap that protects your system.
Fill in
odata_export.py
import csv
import os
import requests
SERVICE = "https://erp.example.com/odata/v4"
ENTITY = "SalesOrders"
MAX_ROWS = 50000
OUTFILE = "sales_orders.csv"
params = {
"$select": "OrderNumber,CustomerId,OrderDate,Status",
"$filter": "Status eq 'Open'",
"$orderby": "OrderNumber asc",
"$top": "500",
}
headers = {
"Authorization": "Bearer " + os.environ["ERP_ODATA_TOKEN"],
"Accept": "application/json",
}
session = requests.Session()
url = SERVICE + "/" + ENTITY
fields = [name.strip() for name in params["$select"].split(",")]
total = 0
with open(OUTFILE, "w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
first = True
while url and total < MAX_ROWS:
# The nextLink already carries all query options, so send params only once
response = session.get(
url, headers=headers, params=params if first else None, timeout=60
)
response.raise_for_status()
payload = response.json()
for row in payload.get("value", []):
writer.writerow(row)
total += 1
if total >= MAX_ROWS:
break
url = payload.get("@odata.nextLink")
first = False
print("Wrote " + str(total) + " rows to " + OUTFILE)
if url:
print("Stopped at the row cap, more pages remain")OData v4 returns @odata.nextLink. OData v2 services such as many SAP Gateway services use d.results and d.__next instead, and $skiptoken or $skip may be needed. Server-driven paging can override your $top. Verify query option support, case sensitivity of field names and filter syntax with your ERP, and test on a non-production system.
ERP data extraction review
Optional: have an engineer check your extract design for load and completeness.
Optional. The tool and every export work without this. No spam.
How to use it
Enter the service root, entity set, fields, a filter and an order. Choose a modest page size and a hard row cap. Export the token into the environment variable and run the script on a test tenant first. Open the CSV to confirm column names. Narrow the filter before you raise the cap, because fewer rows and fewer fields are the easiest ways to reduce load on the ERP.
The method behind it
OData v4 servers return @odata.nextLink when more results exist. That link already includes the query options, so the loop sends parameters only on the first request and then just follows the link. This is more reliable than computing $skip offsets, which can skip or repeat rows if data changes between pages. Stable ordering by a key helps when a service falls back to offsets.
- Use $select to return only the fields you need
- Filter on indexed or key fields where possible
- Keep page sizes moderate, a few hundred to a few thousand
- Cap total rows during testing
- Read-only service account for extracts
ERP notes
Dynamics 365 Finance and Operations, Business Central, SAP Gateway services and Epicor all expose OData with local differences: paging limits, cross-company flags, and how large filters are handled. Check the documented maximum page size and API capacity limits before scheduling this against production. For continuing sync, filter on a modified timestamp instead of re-reading everything.
Common mistakes
Teams send the same parameters with every page request and end up in an endless loop or duplicated rows. Others skip $select and pull wide tables, forget URL encoding for filter values, or run the extract at month end when the ERP is busiest. Always log the row count and stop at a cap.
Frequently Asked Questions
Why follow @odata.nextLink instead of using $skip?
The next link is created by the server and encodes where the next page starts, which stays correct even if rows change between requests. Offset paging with $skip can miss or repeat records on live data and gets slower on large offsets.
Does this work with SAP OData v2?
Not directly. Many v2 services return results under d.results and the next URL under d.__next, and use $skiptoken or $skip. Adjust the two lines that read the rows and the next link. The overall loop is the same.
How big should my page size be?
A few hundred to a few thousand rows is typical. Smaller pages mean more requests, larger pages risk timeouts and memory use. Servers often enforce their own maximum, so test and follow what the service returns.
Can I run this against production?
Yes if you use a read-only account, a narrow filter, a row cap, and an off-peak schedule, and you confirm API capacity limits. Test on a non-production tenant first and watch ERP performance during the first run.
Netray connects AI agents and analytics to ERP data with careful, read-only extraction. If your extracts are slow or risky, we can help redesign them.
Related Tools
Idempotent ERP API Retry Wrapper Python Generator
Fill in your header names and limits to get a Python function that retries safely with backoff and sends an idempotency key on writes.
Developer ToolsHTTP Status Codes for ERP API Agents Reference
Look up an HTTP status code and see what it usually means on an ERP API and whether an agent should retry, fix the request or escalate.
Developer ToolsERP MCP Server Python Tool Skeleton Generator
Get a runnable Python MCP server with one read-only ERP lookup tool, argument validation and errors that the model can understand.
Developer ToolsAgent SQL Read-Only Guard Python Generator
Add a first line of defense before agent-written SQL reaches an ERP database: single statement, SELECT only, allow-listed tables and a row cap.
More Developer Tools tools
See all- SAP Sales Order ABAP SELECT Generator (VBAK, VBAP, VBFA)
- SAP Sales Order Create BAPI ABAP Generator (BAPI_SALESORDER_CREATEFROMDAT2)
- SAP ABAP Open Items SELECT Generator
- SAP ACDOCA SQL Query Generator
- SAP QM Inspection Lot ABAP SELECT Generator
- SAP Quality Notification Pareto SQL Query Generator
- SAP QM Usage Decision BAPI Skeleton
- ABAP Custom Exception Class Generator
- ABAP Unit Test Class Generator