|
14 | 14 | import requests |
15 | 15 | import time |
16 | 16 | from datetime import date, timedelta |
17 | | -from concurrent.futures import ThreadPoolExecutor, as_completed |
18 | 17 |
|
19 | 18 |
|
20 | 19 | entered = input("Enter Dataverse org URL (e.g. https://yourorg.crm.dynamics.com): ").strip() |
@@ -57,10 +56,12 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403 |
57 | 56 | print(f'Request failed: {ex}') |
58 | 57 | last_exc = ex |
59 | 58 | if retry_if and retry_if(ex): |
| 59 | + print("Retrying operation...") |
60 | 60 | continue |
61 | 61 | if isinstance(ex, requests.exceptions.HTTPError): |
62 | 62 | code = getattr(getattr(ex, 'response', None), 'status_code', None) |
63 | 63 | if code in retry_http_statuses: |
| 64 | + print("Retrying operation...") |
64 | 65 | continue |
65 | 66 | break |
66 | 67 | if last_exc: |
@@ -176,20 +177,6 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None: |
176 | 177 | f"count={s.get('count')} amount={s.get('amount')} when={s.get('when')}" |
177 | 178 | ) |
178 | 179 |
|
179 | | -def _resolve_status_value(kind: str, raw_value, use_french: bool): |
180 | | - """kind values: |
181 | | - - 'label': English label |
182 | | - - 'fr_label': French label if allowed, else fallback to English equivalent |
183 | | - - 'int': the enum integer value |
184 | | - """ |
185 | | - if kind == "label": |
186 | | - return raw_value |
187 | | - if kind == "fr_label": |
188 | | - if use_french: |
189 | | - return raw_value |
190 | | - return "Active" if raw_value == "Actif" else "Inactive" |
191 | | - return raw_value |
192 | | - |
193 | 180 | def _has_installed_language(base_url: str, credential, lcid: int) -> bool: |
194 | 181 | try: |
195 | 182 | token = credential.get_token(f"{base_url}/.default").token |
@@ -496,39 +483,60 @@ def run_paging_demo(label: str, *, top: Optional[int], page_size: Optional[int]) |
496 | 483 | print(f"Retrieve multiple demos failed: {e}") |
497 | 484 | # 5) Delete record |
498 | 485 | print("Delete (OData):") |
499 | | -# Show deletes to be executed (concurrently via SDK delete) |
| 486 | +# Show deletes to be executed (single + bulk) |
500 | 487 | if 'record_ids' in locals() and record_ids: |
501 | 488 | print({"delete_count": len(record_ids)}) |
502 | | -pause("Execute Delete (concurrent SDK calls)") |
| 489 | +pause("Execute Delete (single then bulk)") |
503 | 490 | try: |
504 | 491 | if record_ids: |
505 | | - max_workers = min(8, len(record_ids)) |
506 | | - log_call(f"concurrent delete {len(record_ids)} items from '{logical}' (workers={max_workers})") |
| 492 | + single_target = record_ids[0] |
| 493 | + rest_targets = record_ids[1:] |
| 494 | + single_error: Optional[str] = None |
| 495 | + bulk_job_id: Optional[str] = None |
| 496 | + bulk_error: Optional[str] = None |
507 | 497 |
|
508 | | - successes: list[str] = [] |
509 | | - failures: list[dict] = [] |
| 498 | + try: |
| 499 | + log_call(f"client.delete('{logical}', '{single_target}')") |
| 500 | + backoff_retry(lambda: client.delete(logical, single_target)) |
| 501 | + except Exception as ex: |
| 502 | + single_error = str(ex) |
510 | 503 |
|
511 | | - def _del_one(rid: str) -> tuple[str, bool, str | None]: |
512 | | - try: |
513 | | - log_call(f"client.delete('{logical}', '{rid}')") |
514 | | - backoff_retry(lambda: client.delete(logical, rid)) |
515 | | - return (rid, True, None) |
516 | | - except Exception as ex: |
517 | | - return (rid, False, str(ex)) |
518 | | - |
519 | | - with ThreadPoolExecutor(max_workers=max_workers) as executor: |
520 | | - future_map = {executor.submit(_del_one, rid): rid for rid in record_ids} |
521 | | - for fut in as_completed(future_map): |
522 | | - rid, ok, err = fut.result() |
523 | | - if ok: |
524 | | - successes.append(rid) |
525 | | - else: |
526 | | - failures.append({"id": rid, "error": err}) |
| 504 | + half = max(1, len(rest_targets) // 2) |
| 505 | + bulk_targets = rest_targets[:half] |
| 506 | + sequential_targets = rest_targets[half:] |
| 507 | + bulk_error = None |
| 508 | + sequential_error = None |
| 509 | + |
| 510 | + # Fire-and-forget bulk delete for the first portion |
| 511 | + try: |
| 512 | + log_call(f"client.delete('{logical}', <{len(bulk_targets)} ids>, use_bulk_delete=True)") |
| 513 | + bulk_job_id = client.delete(logical, bulk_targets) |
| 514 | + except Exception as ex: |
| 515 | + bulk_error = str(ex) |
| 516 | + |
| 517 | + # Sequential deletes for the remainder |
| 518 | + try: |
| 519 | + log_call(f"client.delete('{logical}', <{len(sequential_targets)} ids>, use_bulk_delete=False)") |
| 520 | + for rid in sequential_targets: |
| 521 | + backoff_retry(lambda rid=rid: client.delete(logical, rid, use_bulk_delete=False)) |
| 522 | + except Exception as ex: |
| 523 | + sequential_error = str(ex) |
527 | 524 |
|
528 | 525 | print({ |
529 | 526 | "entity": logical, |
530 | | - "delete_summary": {"requested": len(record_ids), "success": len(successes), "failures": len(failures)}, |
531 | | - "failed": failures[:5], # preview up to 5 failures |
| 527 | + "delete_single": { |
| 528 | + "id": single_target, |
| 529 | + "error": single_error, |
| 530 | + }, |
| 531 | + "delete_bulk": { |
| 532 | + "count": len(bulk_targets), |
| 533 | + "job_id": bulk_job_id, |
| 534 | + "error": bulk_error, |
| 535 | + }, |
| 536 | + "delete_sequential": { |
| 537 | + "count": len(sequential_targets), |
| 538 | + "error": sequential_error, |
| 539 | + }, |
532 | 540 | }) |
533 | 541 | else: |
534 | 542 | raise RuntimeError("No record created; skipping delete.") |
@@ -577,8 +585,22 @@ def _metadata_after_create(): |
577 | 585 | if isinstance(raw_type, str): |
578 | 586 | attr_type_before = raw_type |
579 | 587 | lowered = raw_type.lower() |
580 | | - log_call(f"client.delete_column('{entity_schema}', '{scratch_column}')") |
581 | | - column_delete = client.delete_columns(entity_schema, scratch_column) |
| 588 | + delete_target = attribute_schema or scratch_column |
| 589 | + log_call(f"client.delete_column('{entity_schema}', '{delete_target}')") |
| 590 | + |
| 591 | + def _delete_column(): |
| 592 | + return client.delete_columns(entity_schema, delete_target) |
| 593 | + |
| 594 | + column_delete = backoff_retry( |
| 595 | + _delete_column, |
| 596 | + delays=(0, 1, 2, 4, 8), |
| 597 | + retry_http_statuses=(), |
| 598 | + retry_if=lambda exc: ( |
| 599 | + isinstance(exc, MetadataError) |
| 600 | + or "not found" in str(exc).lower() |
| 601 | + or "not yet available" in str(exc).lower() |
| 602 | + ), |
| 603 | + ) |
582 | 604 | if not isinstance(column_delete, list) or not column_delete: |
583 | 605 | raise RuntimeError("delete_column did not return schema list") |
584 | 606 | deleted_details = column_delete |
|
0 commit comments