Utilities to build SA-compliant providers easily. You focus on collecting data; SAP handles interval caching and serving it over a tiny HTTP API the SA Shell understands.
pip install -r requirements.txt # ensures FlaskSAP includes comprehensive logging to help you understand what's happening during lazy loading operations. You can configure logging levels and enable debug mode:
from sap import configure_logging
# Basic logging setup
configure_logging(level="INFO")
# Enable debug logging for detailed lazy loading information
configure_logging(level="DEBUG", enable_debug=True)Log Levels:
INFO: General server operations, lazy loading requests, and resultsDEBUG: Detailed lazy loading plans, request parsing, and timing informationWARNING: Non-critical issues like unsupported types or missing conditionsERROR: Critical errors like provider failures or invalid requests
Example Log Output:
2024-10-05 15:11:29 - sap.server - INFO - Received lazy loading request
2024-10-05 15:11:29 - sap.server - INFO - Lazy loading request: type=employee, fields=['favorite_color', 'favorite_number', 'favorite_shape'], conditions=1, plan_only=False
2024-10-05 15:11:29 - sap.server - INFO - Delegating lazy loading to provider function for type: employee
2024-10-05 15:11:29 - sap.server - INFO - Lazy loading completed: 1 objects returned in 0.001s
2024-10-05 15:11:29 - sap.server - DEBUG - Lazy loading plan: Lazy loading employee objects with conditions: [['__id__', '==', 'emp_001']] (data fetched)
# example_provider.py
from sap import SAPServer, make_object, timestamp, link
from datetime import datetime
# Your heavy function: return a list of SA JSON objects (dicts)
def fetch_data():
objs = [
make_object(
id="emp_001",
types=["person", "employee"],
source="my_system",
name="Alice",
hired_at=timestamp(datetime.utcnow()),
profile=link(".filter(.equals(.get_field('name'), 'Alice'))", "Alice's records"),
)
]
return objs
server = SAPServer(
provider=dict(name="My Provider", description="Demo provider"), # or ProviderInfo(...)
fetch_fn=fetch_data,
interval_seconds=300,
)
if __name__ == "__main__":
server.run(port=8080)Endpoints provided:
- GET /hello → provider info with lazy loading scopes
- GET /all_data → cached list of SA objects
- POST /lazy_load → lazy load data with query scopes
interval_secondscontrols how oftenfetch_fnruns. Results are cached and served fast.- Use
make_objectand helpers (timestamp,link) to avoid managing__id__,__source__,__types__and__sa_type__by hand.
- Fixed port by default: defaults to 8080. You can change with
port=or--port. Auto-port fallback is opt-in viaauto_port=Trueor--auto-port. - Health and status:
- GET
/health→{ status: "ok", count: <int> } - GET
/status→ runner timings, error, count
- GET
- Initial fetch control: set
require_initial_fetch=Trueto wait for the first successful fetch before advertising the endpoint. - Register with shell: pass
register_with_shell=Trueto write the URL to~/.sa/saps.txt(deduped).
# Using module entrypoint
python -m sap.cli \
--name "My Provider" \
--description "Demo" \
--fetch mypkg.my_module:build_data \
--interval 300 \
--register
# Or python -m sap (alias)
python -m sap --name "My Provider" --fetch mypkg.my_module:build_dataOptionally protect manual refresh with a token:
export SAP_REFRESH_TOKEN=mysecret
curl "http://localhost:8080/refresh?token=mysecret"from sap import SAPServer
server = SAPServer(
provider=dict(name="My Provider", description="Demo provider"),
fetch_fn=fetch_data,
interval_seconds=300,
)
server.run(register_with_shell=True, require_initial_fetch=True)Each object returned by your fetch_fn must be a dict with at least:
__id__: string__types__: list of strings__source__: string
Optional fields can be any JSON-serializable values. To include SA custom types:
- Use
timestamp(...)to produce{"__sa_type__":"timestamp", "timestamp": <ns>} - Use
link(query, show_text)to produce{"__sa_type__":"link", ...}
You can build objects by hand or via helpers:
from sap import make_object
obj = make_object(
id="123", types=["person"], source="my_db", name="Alice"
)Best practices:
- Keep
__id__stable across runs. - Use a consistent
__source__identifier for your system. - Prefer
make_objectand helpers to avoid subtle schema mistakes.
/healthreturns200JSON{ status: "ok", count }if server is running; it does not reflect fetch failure./statusincludeslast_started_at,last_completed_at,last_error,in_flight,interval_seconds,fetch_timeout_seconds, andcount.
- Fetches never overlap. If a fetch is in-flight when a new interval elapses or
/refreshis called, the new run is skipped. fetch_timeout_seconds(default 120s) limits a single fetch attempt; timeout is recorded inlast_error.
- By default, objects are deduped by
(__id__, __source__, tuple(__types__))after normalization. Provide unique ids for distinct logical records.
- The server runs in a background WSGI thread.
Ctrl+Cor process termination triggers graceful shutdown of the runner and server.
SAP supports lazy loading capabilities that allow clients to request specific data on-demand rather than loading everything at once. This is useful for large datasets or when you want to provide filtered, real-time data.
To enable lazy loading, you need to:
- Define Lazy Loading Scopes: Specify what types of data can be lazy loaded and what fields are available
- Implement a Lazy Load Function: Create a function that handles lazy loading requests
- Configure the Server: Pass the lazy loading configuration to your SAP server
from sap import SAPServer, make_object, timestamp, Scope
from datetime import datetime
def fetch_data():
# Your regular data fetching function
return [
make_object(
id="emp_001",
types=["person", "employee"],
source="hr_system",
name="Alice Johnson",
department="Engineering"
)
]
def lazy_load_data(query_scope: QueryScope, plan_only: bool) -> tuple[list[dict], str]:
"""
Handle lazy loading requests.
Args:
query_scope: Contains the scope (type and fields) and conditions
plan_only: If True, only return the plan without fetching data
Returns:
Tuple of (sa_objects, plan_description)
"""
scope = query_scope.scope
conditions = query_scope.conditions
# Build plan description
plan = f"Lazy loading {scope.type} objects"
if conditions:
plan += f" with conditions: {conditions}"
if plan_only:
plan += " (plan only - no data fetched)"
else:
plan += " (data fetched)"
if plan_only:
return [], plan
# Handle different types of lazy loading
if scope.type == "employee":
# Example: Return employee with additional fields
return [make_object(
id="emp_001",
types=["person", "employee"],
source="hr_system",
favorite_color="blue",
favorite_number=42,
favorite_shape="circle"
)], plan
else:
raise Exception(f"Lazy loading not supported for type: {scope.type}")
# Define lazy loading scopes
lazy_scopes = [
Scope(type="employee", fields=["favorite_color", "favorite_number", "favorite_shape"])
]
server = SAPServer(
provider=dict(
name="My Provider",
description="Demo provider with lazy loading",
lazy_loading_scopes=lazy_scopes
),
fetch_fn=fetch_data,
interval_seconds=300,
lazy_load_fn=lazy_load_data, # Enable lazy loading
)
if __name__ == "__main__":
server.run(port=8080)Returns provider information including available lazy loading scopes:
{
"name": "My Provider",
"description": "Demo provider with lazy loading",
"version": "0.1.0",
"lazy_loading_scopes": [
{
"type": "employee",
"fields": ["favorite_color", "favorite_number", "favorite_shape"]
}
]
}Request specific data using query scopes:
curl -X POST http://localhost:8080/lazy_load \
-H "Content-Type: application/json" \
-d '{
"scope": {
"type": "employee",
"fields": ["favorite_color", "favorite_number", "favorite_shape"]
},
"conditions": [["__id__", "==", "emp_001"]],
"plan_only": false
}'Request Format:
scope: Object withtype(string) andfields(array of strings or "*")conditions: Array of[field, operator, value]tuplesplan_only: Boolean - if true, only return the execution plan without data
Response Format:
{
"sa_objects": [
{
"__id__": "emp_001",
"__types__": ["person", "employee"],
"__source__": "hr_system",
"favorite_color": "blue",
"favorite_number": 42,
"favorite_shape": "circle"
}
],
"plan": "Lazy loading employee objects with conditions: [['__id__', '==', 'emp_001']] (data fetched)"
}def lazy_load_data(query_scope: QueryScope, plan_only: bool) -> tuple[list[dict], str]:
scope = query_scope.scope
conditions = query_scope.conditions
if scope.type == "swipe":
# Require date condition
date_condition = None
for condition in conditions:
field, operator, value = condition
if field == "date":
date_condition = (field, operator, value)
break
if not date_condition:
raise Exception("Swipe queries must include a 'date' condition")
# Generate swipes for the requested date
field, operator, value = date_condition
if operator != "==":
raise Exception("Only '==' operator is supported for date filtering")
target_date = datetime.strptime(value, "%Y-%m-%d").date()
swipes = generate_swipes_for_date(target_date)
return swipes, f"Generated swipes for {value}"
# ... handle other typesdef lazy_load_data(query_scope: QueryScope, plan_only: bool) -> tuple[list[dict], str]:
scope = query_scope.scope
conditions = query_scope.conditions
if scope.type == "employee":
# Require __id__ condition
id_condition = None
for condition in conditions:
field, operator, value = condition
if field == "__id__":
id_condition = (field, operator, value)
break
if not id_condition:
raise Exception("Employee queries must include an '__id__' condition")
# Return employee with additional fields
field, operator, value = id_condition
employee = get_employee_with_details(value)
return [employee], f"Retrieved employee {value}"
# ... handle other types- Validate Conditions: Always check that required conditions are provided
- Handle Errors Gracefully: Return meaningful error messages for invalid requests
- Support Plan-Only Mode: Allow clients to get execution plans without fetching data
- Use Descriptive Plans: Include details about what the lazy loading function will do
- Validate Types: Check that the requested type is supported for lazy loading
- Return Consistent Data: Ensure returned objects follow SA object schema
The lazy loading endpoint returns appropriate HTTP status codes:
200: Success with data400: Bad request (missing conditions, invalid data, etc.)404: Type not supported for lazy loading
Example error responses:
{
"error": "Swipe queries must include a 'date' condition"
}{
"error": "Type 'unsupported_type' not supported for lazy loading"
}You can test your lazy loading implementation using curl or any HTTP client:
# Test plan-only mode
curl -X POST http://localhost:8080/lazy_load \
-H "Content-Type: application/json" \
-d '{
"scope": {"type": "employee", "fields": ["favorite_color"]},
"conditions": [["__id__", "==", "emp_001"]],
"plan_only": true
}'
# Test with invalid type
curl -X POST http://localhost:8080/lazy_load \
-H "Content-Type: application/json" \
-d '{
"scope": {"type": "invalid_type", "fields": ["field1"]},
"conditions": []
}'