Skip to content
Closed
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
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ Auth:
| `delete` | `delete(logical_name, id)` | `None` | Delete one record. |
| `delete` | `delete(logical_name, list[id], use_bulk_delete=True)` | `Optional[str]` | Delete many with async BulkDelete or sequential single-record delete. |
| `query_sql` | `query_sql(sql)` | `list[dict]` | Constrained read-only SELECT via `?sql=`. |
| `create_table` | `create_table(tablename, schema, solution_unique_name=None)` | `dict` | Creates custom table + columns. Friendly name (e.g. `SampleItem`) becomes schema `new_SampleItem`; explicit schema name (contains `_`) used as-is. Pass `solution_unique_name` to attach the table to a specific solution instead of the default solution. |
| `create_column` | `create_column(tablename, columns)` | `list[str]` | Adds columns using a `{name: type}` mapping (same shape as `create_table` schema). Returns schema names for the created columns. |
| `get_table_info` | `get_table_info(schema_name)` | `dict | None` | Basic table metadata by schema name (e.g. `new_SampleItem`). Friendly names not auto-converted. |
| `list_tables` | `list_tables()` | `list[dict]` | Lists non-private tables. |
| `delete_table` | `delete_table(tablename)` | `None` | Drops custom table. Accepts friendly or schema name; friendly converted to `new_<PascalCase>`. |
| `delete_column` | `delete_column(tablename, columns)` | `list[str]` | Deletes one or more columns; returns schema names (accepts string or list[str]). |
| `create_table` | `create_table(logical_name, schema, solution_unique_name=None)` | `dict` | Creates custom table + columns. Requires logical name with publisher prefix (e.g. `new_sampleitem`) and column names with same prefix (e.g. `{"new_code": "string"}`). Pass `solution_unique_name` to attach the table to a specific solution instead of the default solution. |
| `create_columns` | `create_columns(logical_name, columns)` | `list[str]` | Adds columns using a `{name: type}` mapping with publisher prefix (e.g. `{"new_category": "string"}`). Returns logical names for the created columns. |
| `get_table_info` | `get_table_info(logical_name)` | `dict | None` | Basic table metadata by logical name (e.g. `new_sampleitem`). Lookup is case-insensitive. |
| `list_tables` | `list_tables()` | `list[str]` | Lists custom table logical names. |
| `delete_table` | `delete_table(logical_name)` | `None` | Drops custom table by logical name with publisher prefix (e.g. `new_sampleitem`). |
| `delete_columns` | `delete_columns(logical_name, columns)` | `list[str]` | Deletes one or more columns; returns logical names (accepts string or list[str]). |
| `PandasODataClient.create_df` | `create_df(logical_name, series)` | `str` | Create one record (returns GUID). |
| `PandasODataClient.update` | `update(logical_name, id, series)` | `None` | Returns None; ignored if Series empty. |
| `PandasODataClient.get_ids` | `get_ids(logical_name, ids, select=None)` | `DataFrame` | One row per ID (errors inline). |
Expand Down Expand Up @@ -301,21 +301,21 @@ class Status(IntEnum):

# Create a simple custom table and a few columns
info = client.create_table(
"SampleItem", # friendly name; defaults to SchemaName new_SampleItem
"new_sampleitem", # logical name with publisher prefix (e.g., "new_")
{
"code": "string",
"count": "int",
"amount": "decimal",
"when": "datetime",
"active": "bool",
"status": Status,
"new_code": "string",
"new_count": "int",
"new_amount": "decimal",
"new_when": "datetime",
"new_active": "bool",
"new_status": Status,
},
solution_unique_name="my_solution_unique_name", # optional: associate table with this solution
)

# Create or delete columns
client.create_column("SampleItem", {"category": "string"}) # returns ["new_Category"]
client.delete_column("SampleItem", "category") # returns ["new_Category"]
client.create_columns("new_sampleitem", {"new_category": "string"}) # returns ["new_category"]
client.delete_columns("new_sampleitem", "new_category") # returns ["new_category"]

logical = info["entity_logical_name"] # e.g., "new_sampleitem"

Expand All @@ -329,7 +329,7 @@ rec_id = client.create(logical, {name_attr: "Sample A"})[0]

# Clean up
client.delete(logical, rec_id) # delete record
client.delete_table("SampleItem") # delete table (friendly name or explicit schema new_SampleItem)
client.delete_table("new_sampleitem") # delete table by logical name
```

Notes:
Expand Down
22 changes: 11 additions & 11 deletions examples/advanced/file_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,18 +152,18 @@ def backoff(op, *, delays=(0,2,5,10), retry_status=(400,403,404,409,412,429,500,
raise last

# --------------------------- Table ensure ---------------------------
TABLE_SCHEMA_NAME = "new_FileSample"
TABLE_LOGICAL_NAME = "new_filesample"
# If user wants new publisher prefix / naming, adjust above.

def ensure_table():
# Check by schema
existing = client.get_table_info(TABLE_SCHEMA_NAME)
# Check by logical name
existing = client.get_table_info(TABLE_LOGICAL_NAME)
if existing:
print({"table": TABLE_SCHEMA_NAME, "existed": True})
print({"table": TABLE_LOGICAL_NAME, "existed": True})
return existing
log("client.create_table('new_FileSample', schema={title})")
info = client.create_table(TABLE_SCHEMA_NAME, {"title": "string"})
print({"table": TABLE_SCHEMA_NAME, "existed": False, "metadata_id": info.get('metadata_id')})
log(f"client.create_table('{TABLE_LOGICAL_NAME}', schema={{'new_title': 'string'}})")
info = client.create_table(TABLE_LOGICAL_NAME, {"new_title": "string"})
print({"table": TABLE_LOGICAL_NAME, "existed": False, "metadata_id": info.get('metadata_id')})
return info

try:
Expand Down Expand Up @@ -388,8 +388,8 @@ def get_dataset_info(file_path: Path):
# --------------------------- Cleanup ---------------------------
if cleanup_record and record_id:
try:
log(f"client.delete('{entity_set}', '{record_id}')")
backoff(lambda: client.delete(entity_set, record_id))
log(f"client.delete('{logical}', '{record_id}')")
backoff(lambda: client.delete(logical, record_id))
print({"record_deleted": True})
except Exception as e: # noqa: BLE001
print({"record_deleted": False, "error": str(e)})
Expand All @@ -398,8 +398,8 @@ def get_dataset_info(file_path: Path):

if cleanup_table:
try:
log(f"client.delete_table('{TABLE_SCHEMA_NAME}')")
client.delete_table(TABLE_SCHEMA_NAME)
log(f"client.delete_table('{TABLE_LOGICAL_NAME}')")
client.delete_table(TABLE_LOGICAL_NAME)
print({"table_deleted": True})
except Exception as e: # noqa: BLE001
print({"table_deleted": False, "error": str(e)})
Expand Down
24 changes: 12 additions & 12 deletions examples/advanced/pandas_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
table_info = None
created_this_run = False

# First check for existing table
existing = client.get_table_info("SampleItem")
# First check for existing table (use logical name - lowercase with prefix)
existing = client.get_table_info("new_sampleitem")
if existing:
table_info = existing
created_this_run = False
Expand All @@ -68,16 +68,16 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
})

else:
# Create it since it doesn't exist
# Create it since it doesn't exist (use logical name)
try:
table_info = client.create_table(
"SampleItem",
"new_sampleitem",
{
"code": "string",
"count": "int",
"amount": "decimal",
"when": "datetime",
"active": "bool",
"new_code": "string",
"new_count": "int",
"new_amount": "decimal",
"new_when": "datetime",
"new_active": "bool",
},
)
created_this_run = True if table_info and table_info.get("columns_created") else False
Expand Down Expand Up @@ -217,10 +217,10 @@ def _retry_if(ex: Exception) -> bool:
# 6) Cleanup: delete the custom table if it exists
print("Cleanup (Metadata):")
try:
# Delete if present, regardless of whether it was created in this run
info = client.get_table_info("SampleItem")
# Delete if present, regardless of whether it was created in this run (use logical name)
info = client.get_table_info("new_sampleitem")
if info:
client.delete_table("SampleItem")
client.delete_table("new_sampleitem")
print({"table_deleted": True})
else:
print({"table_deleted": False, "reason": "not found"})
Expand Down
65 changes: 35 additions & 30 deletions examples/basic/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ class Status(IntEnum):
# Check for existing table using list_tables
log_call("client.list_tables()")
tables = client.list_tables()
existing_table = next((t for t in tables if t.get("SchemaName") == "new_SampleItem"), None)

# LogicalName is always lowercase (SDK normalizes)
existing_table = next((t for t in tables if t.get("LogicalName", "") == "new_sampleitem"), None)
if existing_table:
table_info = client.get_table_info("new_SampleItem")
table_info = client.get_table_info("new_sampleitem")
created_this_run = False
print({
"table": table_info.get("entity_schema"),
Expand All @@ -106,20 +108,19 @@ class Status(IntEnum):
"logical": table_info.get("entity_logical_name"),
"metadata_id": table_info.get("metadata_id"),
})

else:
# Create it since it doesn't exist
try:
log_call("client.create_table('new_SampleItem', schema={code,count,amount,when,active,status<enum>})")
log_call("client.create_table('new_sampleitem', schema={new_code,new_count,new_amount,new_when,new_active,new_status<enum>})")
table_info = client.create_table(
"new_SampleItem",
"new_sampleitem",
{
"code": "string",
"count": "int",
"amount": "decimal",
"when": "datetime",
"active": "bool",
"status": Status,
"new_code": "string",
"new_count": "int",
"new_amount": "decimal",
"new_when": "datetime",
"new_active": "bool",
"new_status": Status,
},
)
created_this_run = True if table_info and table_info.get("columns_created") else False
Expand All @@ -146,11 +147,11 @@ class Status(IntEnum):
pass
# Fail fast: all operations must use the custom table
sys.exit(1)
entity_schema = table_info.get("entity_schema") or "new_SampleItem"
logical = table_info.get("entity_logical_name")
entity_schema = table_info.get("entity_schema") or "new_Sampleitem"
logical = table_info.get("entity_logical_name") or "new_sampleitem"
metadata_id = table_info.get("metadata_id")
if not metadata_id:
refreshed_info = client.get_table_info(entity_schema) or {}
refreshed_info = client.get_table_info(logical) or {}
metadata_id = refreshed_info.get("metadata_id")
if metadata_id:
table_info["metadata_id"] = metadata_id
Expand Down Expand Up @@ -550,27 +551,28 @@ def run_paging_demo(label: str, *, top: Optional[int], page_size: Optional[int])

# 6) Column metadata helpers: column create/delete
print("Column metadata helpers (create/delete column):")
scratch_column = f"scratch_{int(time.time())}"
scratch_column = f"new_scratch_{int(time.time())}"
column_payload = {scratch_column: "string"}
try:
log_call(f"client.create_column('{entity_schema}', {repr(column_payload)})")
column_create = client.create_columns(entity_schema, column_payload)
log_call(f"client.create_column('{logical}', {repr(column_payload)})")
column_create = client.create_columns(logical, column_payload)
if not isinstance(column_create, list) or not column_create:
raise RuntimeError("create_column did not return schema list")
created_details = column_create
if not all(isinstance(item, str) for item in created_details):
raise RuntimeError("create_column entries were not schema strings")
attribute_schema = created_details[0]
# create_columns returns logical names
odata_client = client._get_odata()
column_logical = created_details[0]
exists_after_create = None
exists_after_delete = None
attr_type_before = None
if metadata_id and attribute_schema:
if metadata_id and column_logical:
_ready_message = "Column metadata not yet available"
def _metadata_after_create():
meta = odata_client._get_attribute_metadata(
metadata_id,
attribute_schema,
column_logical,
extra_select="@odata.type,AttributeType",
)
if not meta or not meta.get("MetadataId"):
Expand All @@ -588,11 +590,12 @@ def _metadata_after_create():
if isinstance(raw_type, str):
attr_type_before = raw_type
lowered = raw_type.lower()
delete_target = attribute_schema or scratch_column
log_call(f"client.delete_column('{entity_schema}', '{delete_target}')")
# For delete, we pass the logical name
delete_target = column_logical
log_call(f"client.delete_column('{logical}', '{delete_target}')")

def _delete_column():
return client.delete_columns(entity_schema, delete_target)
return client.delete_columns(logical, delete_target)

column_delete = backoff_retry(
_delete_column,
Expand All @@ -609,12 +612,14 @@ def _delete_column():
deleted_details = column_delete
if not all(isinstance(item, str) for item in deleted_details):
raise RuntimeError("delete_column entries were not schema strings")
if attribute_schema not in deleted_details:
# deleted_details contains logical names (lowercase), so check for column_logical
if column_logical not in deleted_details:
raise RuntimeError("delete_column response missing expected schema name")
if metadata_id and attribute_schema:
if metadata_id and column_logical:
_delete_message = "Column metadata still present after delete"
def _ensure_removed():
meta = odata_client._get_attribute_metadata(metadata_id, attribute_schema)
# _get_attribute_metadata now accepts logical names
meta = odata_client._get_attribute_metadata(metadata_id, column_logical)
if meta:
raise RuntimeError(_delete_message)
return True
Expand Down Expand Up @@ -645,11 +650,11 @@ def _ensure_removed():
print("Cleanup (Metadata):")
if delete_table_at_end:
try:
log_call("client.get_table_info('new_SampleItem')")
info = client.get_table_info("new_SampleItem")
log_call("client.get_table_info('new_sampleitem')")
info = client.get_table_info("new_sampleitem")
if info:
log_call("client.delete_table('new_SampleItem')")
client.delete_table("new_SampleItem")
log_call("client.delete_table('new_sampleitem')")
client.delete_table("new_sampleitem")
print({"table_deleted": True})
else:
print({"table_deleted": False, "reason": "not found"})
Expand Down
Loading
Loading