Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions examples/advanced/file_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def log(call: str):
_FILE_HASH_CACHE = {}
ATTRIBUTE_VISIBILITY_DELAYS = (0, 3, 10, 20, 35, 50, 70, 90, 120)


def file_sha256(path: Path): # returns (hex_digest, size_bytes)
try:
m = _FILE_HASH_CACHE.get(path)
Expand Down Expand Up @@ -166,19 +167,15 @@ def backoff(op, *, delays=(0, 2, 5, 10, 20, 20)):
result = op()
if attempts > 1:
retry_count = attempts - 1
print(
f" [INFO] Backoff succeeded after {retry_count} retry(s); waited {total_delay}s total."
)
print(f" [INFO] Backoff succeeded after {retry_count} retry(s); waited {total_delay}s total.")
return result
except Exception as ex: # noqa: BLE001
last = ex
continue
if last:
if attempts:
retry_count = max(attempts - 1, 0)
print(
f" [WARN] Backoff exhausted after {retry_count} retry(s); waited {total_delay}s total."
)
print(f" [WARN] Backoff exhausted after {retry_count} retry(s); waited {total_delay}s total.")
raise last


Expand Down Expand Up @@ -227,7 +224,7 @@ def ensure_file_attribute_generic(schema_name: str, label: str, key_prefix: str)
f"{odata.api}/EntityDefinitions({meta_id})/Attributes?$select=SchemaName&$filter="
f"SchemaName eq '{schema_name}'"
)
r = backoff(lambda: odata._request("get", url), delays=ATTRIBUTE_VISIBILITY_DELAYS)
r, _ = backoff(lambda: odata._request("get", url), delays=ATTRIBUTE_VISIBILITY_DELAYS)
val = []
try:
val = r.json().get("value", [])
Expand Down Expand Up @@ -255,7 +252,7 @@ def ensure_file_attribute_generic(schema_name: str, label: str, key_prefix: str)
}
try:
url = f"{odata.api}/EntityDefinitions({meta_id})/Attributes"
backoff(lambda: odata._request("post", url, json=payload), delays=ATTRIBUTE_VISIBILITY_DELAYS)
backoff(lambda: odata._request("post", url, json=payload)[0], delays=ATTRIBUTE_VISIBILITY_DELAYS)
print({f"{key_prefix}_file_attribute_created": True})
time.sleep(2)
return True
Expand Down Expand Up @@ -285,7 +282,7 @@ def wait_for_attribute_visibility(logical_name: str, label: str):
time.sleep(delay)
waited += delay
try:
resp = odata._request("get", probe_url)
resp, _ = odata._request("get", probe_url)
try:
resp.json()
except Exception: # noqa: BLE001
Expand Down Expand Up @@ -313,7 +310,7 @@ def wait_for_attribute_visibility(logical_name: str, label: str):
payload = {name_attr: "File Sample Record"}
log(f"client.create('{table_schema_name}', payload)")
created_ids = backoff(lambda: client.create(table_schema_name, payload))
if isinstance(created_ids, list) and created_ids:
if created_ids and len(created_ids) > 0:
record_id = created_ids[0]
else:
raise RuntimeError("Unexpected create return; expected list[str] with at least one GUID")
Expand Down Expand Up @@ -363,7 +360,7 @@ def get_dataset_info(file_path: Path):
dl_url_single = (
f"{odata.api}/{entity_set}({record_id})/{small_file_attr_logical}/$value" # raw entity_set URL OK
)
resp_single = backoff(lambda: odata._request("get", dl_url_single))
resp_single, _ = backoff(lambda: odata._request("get", dl_url_single))
content_single = resp_single.content or b""
import hashlib # noqa: WPS433

Expand Down Expand Up @@ -393,7 +390,7 @@ def get_dataset_info(file_path: Path):
)
)
print({"small_replace_upload_completed": True, "small_replace_source_size": replace_size_small})
resp_single_replace = backoff(lambda: odata._request("get", dl_url_single))
resp_single_replace, _ = backoff(lambda: odata._request("get", dl_url_single))
content_single_replace = resp_single_replace.content or b""
downloaded_hash_replace = hashlib.sha256(content_single_replace).hexdigest() if content_single_replace else None
hash_match_replace = (
Expand Down Expand Up @@ -435,7 +432,7 @@ def get_dataset_info(file_path: Path):
dl_url_chunk = (
f"{odata.api}/{entity_set}({record_id})/{chunk_file_attr_logical}/$value" # raw entity_set for download
)
resp_chunk = backoff(lambda: odata._request("get", dl_url_chunk))
resp_chunk, _ = backoff(lambda: odata._request("get", dl_url_chunk))
content_chunk = resp_chunk.content or b""
import hashlib # noqa: WPS433

Expand Down Expand Up @@ -464,7 +461,7 @@ def get_dataset_info(file_path: Path):
)
)
print({"chunk_replace_upload_completed": True})
resp_chunk_replace = backoff(lambda: odata._request("get", dl_url_chunk))
resp_chunk_replace, _ = backoff(lambda: odata._request("get", dl_url_chunk))
content_chunk_replace = resp_chunk_replace.content or b""
dst_hash_chunk_replace = hashlib.sha256(content_chunk_replace).hexdigest() if content_chunk_replace else None
hash_match_chunk_replace = (
Expand Down
73 changes: 48 additions & 25 deletions examples/advanced/walkthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def log_call(description):
print(f"\n-> {description}")


def print_telemetry(telemetry):
"""Print telemetry IDs from a response."""
print(f" Telemetry:")
print(f" client_request_id: {telemetry.get('client_request_id')}")
print(f" correlation_id: {telemetry.get('correlation_id')}")
print(f" service_request_id: {telemetry.get('service_request_id')}")


# Define enum for priority picklist
class Priority(IntEnum):
LOW = 1
Expand All @@ -52,19 +60,15 @@ def backoff(op, *, delays=(0, 2, 5, 10, 20, 20)):
result = op()
if attempts > 1:
retry_count = attempts - 1
print(
f" [INFO] Backoff succeeded after {retry_count} retry(s); waited {total_delay}s total."
)
print(f" [INFO] Backoff succeeded after {retry_count} retry(s); waited {total_delay}s total.")
return result
except Exception as ex: # noqa: BLE001
last = ex
continue
if last:
if attempts:
retry_count = max(attempts - 1, 0)
print(
f" [WARN] Backoff exhausted after {retry_count} retry(s); waited {total_delay}s total."
)
print(f" [WARN] Backoff exhausted after {retry_count} retry(s); waited {total_delay}s total.")
raise last


Expand Down Expand Up @@ -104,12 +108,13 @@ def main():
table_name = "new_WalkthroughDemo"

log_call(f"client.get_table_info('{table_name}')")
table_info = backoff(lambda: client.get_table_info(table_name))
table_info_result = backoff(lambda: client.get_table_info(table_name))
print_telemetry(table_info_result.with_response_details().telemetry)

if table_info:
print(f"[OK] Table already exists: {table_info.get('table_schema_name')}")
print(f" Logical Name: {table_info.get('table_logical_name')}")
print(f" Entity Set: {table_info.get('entity_set_name')}")
if table_info_result.value:
print(f"[OK] Table already exists: {table_info_result.get('table_schema_name')}")
print(f" Logical Name: {table_info_result.get('table_logical_name')}")
print(f" Entity Set: {table_info_result.get('entity_set_name')}")
else:
log_call(f"client.create_table('{table_name}', columns={{...}})")
columns = {
Expand All @@ -119,9 +124,10 @@ def main():
"new_Completed": "bool",
"new_Priority": Priority,
}
table_info = backoff(lambda: client.create_table(table_name, columns))
print(f"[OK] Created table: {table_info.get('table_schema_name')}")
print(f" Columns created: {', '.join(table_info.get('columns_created', []))}")
table_info_result = backoff(lambda: client.create_table(table_name, columns))
print(f"[OK] Created table: {table_info_result.get('table_schema_name')}")
print(f" Columns created: {', '.join(table_info_result.get('columns_created', []))}")
print_telemetry(table_info_result.with_response_details().telemetry)

# ============================================================================
# 3. CREATE OPERATIONS
Expand All @@ -139,8 +145,10 @@ def main():
"new_Completed": False,
"new_Priority": Priority.MEDIUM,
}
id1 = backoff(lambda: client.create(table_name, single_record))[0]
create_result = backoff(lambda: client.create(table_name, single_record))
id1 = create_result[0]
print(f"[OK] Created single record: {id1}")
print_telemetry(create_result.with_response_details().telemetry)

# Multiple create
log_call(f"client.create('{table_name}', [{{...}}, {{...}}, {{...}}])")
Expand Down Expand Up @@ -169,6 +177,7 @@ def main():
]
ids = backoff(lambda: client.create(table_name, multiple_records))
print(f"[OK] Created {len(ids)} records: {ids}")
print_telemetry(ids.with_response_details().telemetry)

# ============================================================================
# 4. READ OPERATIONS
Expand All @@ -195,13 +204,15 @@ def main():
indent=2,
)
)
print_telemetry(record.with_response_details().telemetry)

# Multiple read with filter
log_call(f"client.get('{table_name}', filter='new_quantity gt 5')")
all_records = []
records_iterator = backoff(lambda: client.get(table_name, filter="new_quantity gt 5"))
for page in records_iterator:
all_records.extend(page)
print_telemetry(page.with_response_details().telemetry)
print(f"[OK] Found {len(all_records)} records with new_quantity > 5")
for rec in all_records:
print(f" - new_Title='{rec.get('new_title')}', new_Quantity={rec.get('new_quantity')}")
Expand All @@ -215,14 +226,16 @@ def main():

# Single update
log_call(f"client.update('{table_name}', '{id1}', {{...}})")
backoff(lambda: client.update(table_name, id1, {"new_Quantity": 100}))
update_result = backoff(lambda: client.update(table_name, id1, {"new_Quantity": 100}))
updated = backoff(lambda: client.get(table_name, id1))
print(f"[OK] Updated single record new_Quantity: {updated.get('new_quantity')}")
print_telemetry(update_result.with_response_details().telemetry)

# Multiple update (broadcast same change)
log_call(f"client.update('{table_name}', [{len(ids)} IDs], {{...}})")
backoff(lambda: client.update(table_name, ids, {"new_Completed": True}))
multi_update_result = backoff(lambda: client.update(table_name, ids, {"new_Completed": True}))
print(f"[OK] Updated {len(ids)} records to new_Completed=True")
print_telemetry(multi_update_result.with_response_details().telemetry)

# ============================================================================
# 6. PAGING DEMO
Expand All @@ -245,6 +258,7 @@ def main():
]
paging_ids = backoff(lambda: client.create(table_name, paging_records))
print(f"[OK] Created {len(paging_ids)} records for paging demo")
print_telemetry(paging_ids.with_response_details().telemetry)

# Query with paging
log_call(f"client.get('{table_name}', page_size=5)")
Expand All @@ -253,6 +267,7 @@ def main():
for page_num, page in enumerate(paging_iterator, start=1):
record_ids = [r.get("new_walkthroughdemoid")[:8] + "..." for r in page]
print(f" Page {page_num}: {len(page)} records - IDs: {record_ids}")
print_telemetry(page.with_response_details().telemetry)

# ============================================================================
# 7. SQL QUERY
Expand All @@ -264,10 +279,11 @@ def main():
log_call(f"client.query_sql('SELECT new_title, new_quantity FROM {table_name} WHERE new_completed = 1')")
sql = f"SELECT new_title, new_quantity FROM new_walkthroughdemo WHERE new_completed = 1"
try:
results = backoff(lambda: client.query_sql(sql))
print(f"[OK] SQL query returned {len(results)} completed records:")
for result in results[:5]: # Show first 5
sql_result = backoff(lambda: client.query_sql(sql))
print(f"[OK] SQL query returned {len(sql_result)} completed records:")
for result in sql_result[:5]: # Show first 5
print(f" - new_Title='{result.get('new_title')}', new_Quantity={result.get('new_quantity')}")
print_telemetry(sql_result.with_response_details().telemetry)
except Exception as e:
print(f"[WARN] SQL query failed (known server-side bug): {str(e)}")

Expand All @@ -286,11 +302,13 @@ def main():
"new_Completed": False,
"new_Priority": "High", # String label instead of int
}
label_id = backoff(lambda: client.create(table_name, label_record))[0]
label_create_result = backoff(lambda: client.create(table_name, label_record))
label_id = label_create_result[0]
retrieved = backoff(lambda: client.get(table_name, label_id))
print(f"[OK] Created record with string label 'High' for new_Priority")
print(f" new_Priority stored as integer: {retrieved.get('new_priority')}")
print(f" new_Priority@FormattedValue: {retrieved.get('new_priority@OData.Community.Display.V1.FormattedValue')}")
print_telemetry(label_create_result.with_response_details().telemetry)

# ============================================================================
# 9. COLUMN MANAGEMENT
Expand All @@ -302,11 +320,13 @@ def main():
log_call(f"client.create_columns('{table_name}', {{'new_Notes': 'string'}})")
created_cols = backoff(lambda: client.create_columns(table_name, {"new_Notes": "string"}))
print(f"[OK] Added column: {created_cols[0]}")
print_telemetry(created_cols.with_response_details().telemetry)

# Delete the column we just added
log_call(f"client.delete_columns('{table_name}', ['new_Notes'])")
backoff(lambda: client.delete_columns(table_name, ["new_Notes"]))
delete_cols_result = backoff(lambda: client.delete_columns(table_name, ["new_Notes"]))
print(f"[OK] Deleted column: new_Notes")
print_telemetry(delete_cols_result.with_response_details().telemetry)

# ============================================================================
# 10. DELETE OPERATIONS
Expand All @@ -317,14 +337,16 @@ def main():

# Single delete
log_call(f"client.delete('{table_name}', '{id1}')")
backoff(lambda: client.delete(table_name, id1))
delete_result = backoff(lambda: client.delete(table_name, id1))
print(f"[OK] Deleted single record: {id1}")
print_telemetry(delete_result.with_response_details().telemetry)

# Multiple delete (delete the paging demo records)
log_call(f"client.delete('{table_name}', [{len(paging_ids)} IDs])")
job_id = backoff(lambda: client.delete(table_name, paging_ids))
print(f"[OK] Bulk delete job started: {job_id}")
print(f" (Deleting {len(paging_ids)} paging demo records)")
print_telemetry(job_id.with_response_details().telemetry)

# ============================================================================
# 11. CLEANUP
Expand All @@ -335,11 +357,12 @@ def main():

log_call(f"client.delete_table('{table_name}')")
try:
backoff(lambda: client.delete_table(table_name))
delete_table_result = backoff(lambda: client.delete_table(table_name))
print(f"[OK] Deleted table: {table_name}")
print_telemetry(delete_table_result.with_response_details().telemetry)
except Exception as ex: # noqa: BLE001
code = getattr(getattr(ex, "response", None), "status_code", None)
if (isinstance(ex, (requests.exceptions.HTTPError, MetadataError)) and code == 404):
if isinstance(ex, (requests.exceptions.HTTPError, MetadataError)) and code == 404:
print(f"[OK] Table removed: {table_name}")
else:
raise
Expand Down
26 changes: 7 additions & 19 deletions examples/basic/functional_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,16 @@ def wait_for_table_metadata(
odata._entity_set_from_schema_name(table_schema_name)

if attempt > 1:
print(
f" [OK] Table metadata available after {attempt} attempts."
)
print(f" [OK] Table metadata available after {attempt} attempts.")
return info
except Exception:
pass

if attempt < retries:
print(
f" Waiting for table metadata to publish (attempt {attempt}/{retries})..."
)
print(f" Waiting for table metadata to publish (attempt {attempt}/{retries})...")
time.sleep(delay_seconds)

raise RuntimeError(
"Table metadata did not become available in time. Please retry later."
)
raise RuntimeError("Table metadata did not become available in time. Please retry later.")


def ensure_test_table(client: DataverseClient) -> Dict[str, Any]:
Expand Down Expand Up @@ -190,7 +184,7 @@ def test_create_record(client: DataverseClient, table_info: Dict[str, Any]) -> s
continue
raise

if isinstance(created_ids, list) and created_ids:
if created_ids and len(created_ids) > 0:
record_id = created_ids[0]
print(f"[OK] Record created successfully!")
print(f" Record ID: {record_id}")
Expand Down Expand Up @@ -229,9 +223,7 @@ def test_read_record(client: DataverseClient, table_info: Dict[str, Any], record
break
except HttpError as err:
if getattr(err, "status_code", None) == 404 and attempt < retries:
print(
f" Record not queryable yet (attempt {attempt}/{retries}). Retrying in {delay_seconds}s..."
)
print(f" Record not queryable yet (attempt {attempt}/{retries}). Retrying in {delay_seconds}s...")
time.sleep(delay_seconds)
continue
raise
Expand Down Expand Up @@ -301,9 +293,7 @@ def test_query_records(client: DataverseClient, table_info: Dict[str, Any]) -> N
break
except HttpError as err:
if getattr(err, "status_code", None) == 404 and attempt < retries:
print(
f" Query retry {attempt}/{retries} after metadata 404 ({err}). Waiting {delay_seconds}s..."
)
print(f" Query retry {attempt}/{retries} after metadata 404 ({err}). Waiting {delay_seconds}s...")
time.sleep(delay_seconds)
continue
raise
Expand Down Expand Up @@ -373,9 +363,7 @@ def cleanup_test_data(client: DataverseClient, table_info: Dict[str, Any], recor
print("[OK] Test table deleted successfully (404 reported).")
break
if attempt < retries:
print(
f" Table delete retry {attempt}/{retries} after error ({err}). Waiting {delay_seconds}s..."
)
print(f" Table delete retry {attempt}/{retries} after error ({err}). Waiting {delay_seconds}s...")
time.sleep(delay_seconds)
continue
print(f"[WARN] Failed to delete test table: {err}")
Expand Down
Loading
Loading