diff --git a/.claude/skills/dataverse-sdk-use/SKILL.md b/.claude/skills/dataverse-sdk-use/SKILL.md index 7f113294..ce74fc01 100644 --- a/.claude/skills/dataverse-sdk-use/SKILL.md +++ b/.claude/skills/dataverse-sdk-use/SKILL.md @@ -271,9 +271,6 @@ df = client.query.builder("account").select("name").top(100).execute().to_datafr # Via records.list() (simpler for basic queries) df = client.records.list("account", filter="statecode eq 0", select=["name"]).to_dataframe() -# Fetch single record as one-row DataFrame -df = client.records.retrieve("account", account_id, select=["name"]).to_dataframe() - # Create records from a DataFrame (returns a Series of GUIDs) new_accounts = pd.DataFrame([ {"name": "Contoso", "telephone1": "555-0100"}, diff --git a/README.md b/README.md index 3adada6a..91e1fc92 100644 --- a/README.md +++ b/README.md @@ -297,9 +297,6 @@ print(f"Found {len(df)} accounts") # Limit results with top for large tables df = client.query.builder("account").select("name").top(100).execute().to_dataframe() -# Fetch a single record as a one-row DataFrame -df = client.records.retrieve("account", account_id, select=["name"]).to_dataframe() - # Create records from a DataFrame (returns a Series of GUIDs) new_accounts = pd.DataFrame([ {"name": "Contoso", "telephone1": "555-0100"}, @@ -1086,6 +1083,44 @@ Each log file is timestamped and rotated automatically (default 10 MB per file, > Delete logs after the debugging session; use secure deletion for regulated data. > - **Prevent source control leaks.** Add the log folder to `.gitignore` immediately. +### HTTP timeouts and retries + +The client applies sensible per-method HTTP timeouts and automatically retries +transient network errors. You can tune both via `DataverseConfig`. + +| Setting | Default | Applies to | +|---------|---------|------------| +| `http_timeout` | per-method (see below) | every request — overrides the per-method defaults when set | +| `http_retries` | `5` | maximum attempts per request on network errors (`requests.exceptions.RequestException`) | +| `http_backoff` | `0.5` | base delay in seconds between retries; doubles each attempt (`0.5s, 1s, 2s, 4s, …`) | + +When `http_timeout` is not set, the client uses: + +- **10 seconds** for `GET` (and any non-write method) +- **120 seconds** for `POST`, `PATCH`, `DELETE` + +The 10s read default is comfortable for routine data queries but can be too tight +for large metadata reads (e.g. `client.tables.list_relationships()`, +`client.tables.list_columns()`) on orgs with many tables/relationships, or on the +first call after an org wakes from idle. If you see `ReadTimeout` errors from +those endpoints, raise the ceiling: + +```python +from PowerPlatform.Dataverse.client import DataverseClient +from PowerPlatform.Dataverse.core import DataverseConfig + +config = DataverseConfig( + http_timeout=120, # seconds — applies to every request + http_retries=3, # cap retries on slow metadata calls + http_backoff=1.0, +) +client = DataverseClient("https://yourorg.crm.dynamics.com", credential, config=config) +``` + +> **Note:** Setting `http_timeout` overrides the per-method defaults for **all** +> requests, not just metadata calls. Pick a value large enough for the slowest +> operation you expect (typically metadata listing or bulk writes). + ### Limitations - SQL queries are **read-only** and support a limited subset of SQL syntax diff --git a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md index 15b6c477..459a06a5 100644 --- a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md +++ b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md @@ -271,9 +271,6 @@ df = client.query.builder("account").select("name").top(100).execute().to_datafr # Via records.list() (simpler for basic queries) df = client.records.list("account", filter="statecode eq 0", select=["name"]).to_dataframe() -# Fetch single record as one-row DataFrame -df = client.records.retrieve("account", account_id, select=["name"]).to_dataframe() - # Create records from a DataFrame (returns a Series of GUIDs) new_accounts = pd.DataFrame([ {"name": "Contoso", "telephone1": "555-0100"}, diff --git a/src/PowerPlatform/Dataverse/models/filters.py b/src/PowerPlatform/Dataverse/models/filters.py index 9a4df3c2..af0eb985 100644 --- a/src/PowerPlatform/Dataverse/models/filters.py +++ b/src/PowerPlatform/Dataverse/models/filters.py @@ -234,7 +234,7 @@ def __init__(self, column: str, values: Collection[Any]) -> None: def to_odata(self) -> str: # PropertyValues is Collection(Edm.String) - parts = [f'"{_format_value(v).strip("'")}"' for v in self.values] + parts = [f'"{s}"' for s in (_format_value(v).strip("'") for v in self.values)] formatted = ",".join(parts) return f"Microsoft.Dynamics.CRM.In" f"(PropertyName='{self.column}',PropertyValues=[{formatted}])" @@ -252,7 +252,7 @@ def __init__(self, column: str, values: Collection[Any]) -> None: def to_odata(self) -> str: # Same Collection(Edm.String) rules as _InFilter. - parts = [f'"{_format_value(v).strip("'")}"' for v in self.values] + parts = [f'"{s}"' for s in (_format_value(v).strip("'") for v in self.values)] formatted = ",".join(parts) return f"Microsoft.Dynamics.CRM.NotIn" f"(PropertyName='{self.column}',PropertyValues=[{formatted}])" diff --git a/tests/unit/test_migration_tool.py b/tests/unit/test_migration_tool.py index 0446b45b..1dc2da23 100644 --- a/tests/unit/test_migration_tool.py +++ b/tests/unit/test_migration_tool.py @@ -1286,10 +1286,12 @@ def test_crlf_source_stays_crlf(self): out = self._write_bytes_and_migrate(src) # Every newline in the output must still be CRLF: every \n preceded by \r, # equivalently CR count == LF count. + cr_count = out.count(b"\r") + lf_count = out.count(b"\n") self.assertEqual( - out.count(b"\r"), - out.count(b"\n"), - f"CRLF source must stay CRLF; got CR={out.count(b'\\r')} LF={out.count(b'\\n')}", + cr_count, + lf_count, + f"CRLF source must stay CRLF; got CR={cr_count} LF={lf_count}", ) # Sanity: the actual rewrite is visible. self.assertIn(b".where(col(", out)