Skip to content

Commit 0226a22

Browse files
Merge pull request #18 from microsoft/users/zhaodongwang/optionSet
Users/zhaodongwang/option set
2 parents f9d5ebf + 84b1bfa commit 0226a22

5 files changed

Lines changed: 624 additions & 22 deletions

File tree

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A Python package allowing developers to connect to Dataverse environments for DD
1616

1717
- Simple `DataverseClient` facade for CRUD, SQL (read-only), and table metadata.
1818
- SQL-over-API: Constrained SQL (single SELECT with limited WHERE/TOP/ORDER BY) via native Web API `?sql=` parameter.
19-
- Table metadata ops: create simple custom tables with primitive columns (string/int/decimal/float/datetime/bool) and delete them.
19+
- Table metadata ops: create simple custom tables (supports string/int/decimal/float/datetime/bool/optionset) and delete them.
2020
- Bulk create via `CreateMultiple` (collection-bound) by passing `list[dict]` to `create(entity_set, payloads)`; returns list of created IDs.
2121
- Bulk update via `UpdateMultiple` (invoked internally) by calling unified `update(entity_set, ids, patch|patches)`; returns nothing.
2222
- Retrieve multiple with server-driven paging: `get_multiple(...)` yields lists (pages) following `@odata.nextLink`. Control total via `$top` and per-page via `page_size` (Prefer: `odata.maxpagesize`).
@@ -289,7 +289,25 @@ for page in pages: # page is list[dict]
289289
### Custom table (metadata) example
290290

291291
```python
292-
# Create a simple custom table and a few primitive columns
292+
# Support enums with labels in different languages
293+
class Status(IntEnum):
294+
Active = 1
295+
Inactive = 2
296+
Archived = 5
297+
__labels__ = {
298+
1033: {
299+
"Active": "Active",
300+
"Inactive": "Inactive",
301+
"Archived": "Archived",
302+
},
303+
1036: {
304+
"Active": "Actif",
305+
"Inactive": "Inactif",
306+
"Archived": "Archivé",
307+
}
308+
}
309+
310+
# Create a simple custom table and a few columns
293311
info = client.create_table(
294312
"SampleItem", # friendly name; defaults to SchemaName new_SampleItem
295313
{
@@ -298,6 +316,7 @@ info = client.create_table(
298316
"amount": "decimal",
299317
"when": "datetime",
300318
"active": "bool",
319+
"status": Status,
301320
},
302321
)
303322

examples/quickstart.py

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
sys.path.append(str(Path(__file__).resolve().parents[1] / "src"))
88

99
from dataverse_sdk import DataverseClient
10+
from enum import IntEnum
1011
from azure.identity import InteractiveBrowserCredential
1112
import traceback
1213
import requests
@@ -64,6 +65,24 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
6465
if last_exc:
6566
raise last_exc
6667

68+
# Enum demonstrating local option set creation with multilingual labels (for French labels to work, enable French language in the environment first)
69+
class Status(IntEnum):
70+
Active = 1
71+
Inactive = 2
72+
Archived = 5
73+
__labels__ = {
74+
1033: {
75+
"Active": "Active",
76+
"Inactive": "Inactive",
77+
"Archived": "Archived",
78+
},
79+
1036: {
80+
"Active": "Actif",
81+
"Inactive": "Inactif",
82+
"Archived": "Archivé",
83+
}
84+
}
85+
6786
print("Ensure custom table exists (Metadata):")
6887
table_info = None
6988
created_this_run = False
@@ -86,7 +105,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
86105
else:
87106
# Create it since it doesn't exist
88107
try:
89-
log_call("client.create_table('new_SampleItem', schema={code,count,amount,when,active})")
108+
log_call("client.create_table('new_SampleItem', schema={code,count,amount,when,active,status<enum>})")
90109
table_info = client.create_table(
91110
"new_SampleItem",
92111
{
@@ -95,6 +114,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
95114
"amount": "decimal",
96115
"when": "datetime",
97116
"active": "bool",
117+
"status": Status,
98118
},
99119
)
100120
created_this_run = True if table_info and table_info.get("columns_created") else False
@@ -130,6 +150,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
130150
count_key = f"{attr_prefix}_count"
131151
amount_key = f"{attr_prefix}_amount"
132152
when_key = f"{attr_prefix}_when"
153+
status_key = f"{attr_prefix}_status"
133154
id_key = f"{logical}id"
134155

135156
def summary_from_record(rec: dict) -> dict:
@@ -148,6 +169,46 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
148169
f"count={s.get('count')} amount={s.get('amount')} when={s.get('when')}"
149170
)
150171

172+
def _resolve_status_value(kind: str, raw_value, use_french: bool):
173+
"""kind values:
174+
- 'label': English label
175+
- 'fr_label': French label if allowed, else fallback to English equivalent
176+
- 'int': the enum integer value
177+
"""
178+
if kind == "label":
179+
return raw_value
180+
if kind == "fr_label":
181+
if use_french:
182+
return raw_value
183+
return "Active" if raw_value == "Actif" else "Inactive"
184+
return raw_value
185+
186+
def _has_installed_language(base_url: str, credential, lcid: int) -> bool:
187+
try:
188+
token = credential.get_token(f"{base_url}/.default").token
189+
url = f"{base_url}/api/data/v9.2/RetrieveAvailableLanguages()"
190+
headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
191+
resp = requests.get(url, headers=headers, timeout=15)
192+
if not resp.ok:
193+
return False
194+
data = resp.json() if resp.content else {}
195+
langs: list[int] = []
196+
for val in data.values():
197+
if isinstance(val, list) and val and all(isinstance(x, int) for x in val):
198+
langs = val
199+
break
200+
print({"lang_check": {"endpoint": url, "status": resp.status_code, "found": langs, "using": lcid in langs}})
201+
return lcid in langs
202+
except Exception:
203+
return False
204+
205+
# if French language (1036) is installed, we use labels in both English and French
206+
use_french_labels = _has_installed_language(base_url, credential, 1036)
207+
if use_french_labels:
208+
print({"labels_language": "fr", "note": "French labels in use."})
209+
else:
210+
print({"labels_language": "en", "note": "Using English (and numeric values)."})
211+
151212
# 2) Create a record in the new table
152213
print("Create records (OData) demonstrating single create and bound CreateMultiple (multi):")
153214

@@ -159,21 +220,42 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
159220
amount_key: 123.45,
160221
when_key: "2025-01-01",
161222
f"{attr_prefix}_active": True,
223+
status_key: ("Actif" if use_french_labels else Status.Active.value),
162224
}
163225
# Generate multiple payloads
226+
# Distribution update: roughly one-third English labels, one-third French labels, one-third raw integer values.
227+
# We cycle per record: index % 3 == 1 -> English label, == 2 -> French label (if available, else English), == 0 -> integer value.
164228
multi_payloads: list[dict] = []
165229
base_date = date(2025, 1, 2)
230+
# Fixed 6-step cycle pattern encapsulated in helper: Active, Inactive, Actif, Inactif, 1, 2 (repeat)
231+
def _status_value_for_index(idx: int, use_french: bool):
232+
pattern = [
233+
("label", "Active"),
234+
("label", "Inactive"),
235+
("fr_label", "Actif"),
236+
("fr_label", "Inactif"),
237+
("int", Status.Active.value),
238+
("int", Status.Inactive.value),
239+
]
240+
kind, raw = pattern[(idx - 1) % len(pattern)]
241+
if kind == "label":
242+
return raw
243+
if kind == "fr_label":
244+
if use_french:
245+
return raw
246+
return "Active" if raw == "Actif" else "Inactive"
247+
return raw
248+
166249
for i in range(1, 16):
167-
multi_payloads.append(
168-
{
169-
f"{attr_prefix}_name": f"Sample {i:02d}",
170-
code_key: f"X{200 + i:03d}",
171-
count_key: 5 * i,
172-
amount_key: round(10.0 * i, 2),
173-
when_key: (base_date + timedelta(days=i - 1)).isoformat(),
174-
f"{attr_prefix}_active": True,
175-
}
176-
)
250+
multi_payloads.append({
251+
f"{attr_prefix}_name": f"Sample {i:02d}",
252+
code_key: f"X{200 + i:03d}",
253+
count_key: 5 * i,
254+
amount_key: round(10.0 * i, 2),
255+
when_key: (base_date + timedelta(days=i - 1)).isoformat(),
256+
f"{attr_prefix}_active": True,
257+
status_key: _status_value_for_index(i, use_french_labels),
258+
})
177259

178260
record_ids: list[str] = []
179261

@@ -239,11 +321,13 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
239321
f"{attr_prefix}_amount": 543.21,
240322
f"{attr_prefix}_when": "2025-02-02",
241323
f"{attr_prefix}_active": False,
324+
status_key: ("Inactif" if use_french_labels else Status.Inactive.value),
242325
}
243326
expected_checks = {
244327
f"{attr_prefix}_code": "X002",
245328
f"{attr_prefix}_count": 99,
246329
f"{attr_prefix}_active": False,
330+
status_key: Status.Inactive.value,
247331
}
248332
amount_key = f"{attr_prefix}_amount"
249333

@@ -356,7 +440,7 @@ def run_paging_demo(label: str, *, top: Optional[int], page_size: Optional[int])
356440
print({"paging_demo": label, "top": top, "page_size": page_size})
357441
total = 0
358442
page_index = 0
359-
_select = [id_key, code_key, amount_key, when_key]
443+
_select = [id_key, code_key, amount_key, when_key, status_key]
360444
_orderby = [f"{code_key} asc"]
361445
for page in client.get_multiple(
362446
entity_set,
@@ -373,7 +457,13 @@ def run_paging_demo(label: str, *, top: Optional[int], page_size: Optional[int])
373457
"page": page_index,
374458
"page_size": len(page),
375459
"sample": [
376-
{"id": r.get(id_key), "code": r.get(code_key), "amount": r.get(amount_key), "when": r.get(when_key)}
460+
{
461+
"id": r.get(id_key),
462+
"code": r.get(code_key),
463+
"amount": r.get(amount_key),
464+
"when": r.get(when_key),
465+
"status": r.get(status_key),
466+
}
377467
for r in page[:5]
378468
],
379469
})

src/dataverse_sdk/client.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ def _get_odata(self) -> ODataClient:
5959
The lazily-initialized low-level client used to perform requests.
6060
"""
6161
if self._odata is None:
62-
self._odata = ODataClient(self.auth, self._base_url, self._config)
62+
self._odata = ODataClient(
63+
self.auth,
64+
self._base_url,
65+
self._config,
66+
)
6367
return self._odata
6468

6569
# ---------------- Unified CRUD: create/update/delete ----------------
@@ -206,16 +210,19 @@ def get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]:
206210
"""
207211
return self._get_odata()._get_table_info(tablename)
208212

209-
def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any]:
213+
def create_table(self, tablename: str, schema: Dict[str, Any]) -> Dict[str, Any]:
210214
"""Create a simple custom table.
211215
212216
Parameters
213217
----------
214218
tablename : str
215219
Friendly name (``"SampleItem"``) or a full schema name (``"new_SampleItem"``).
216-
schema : dict[str, str]
220+
schema : dict[str, Any]
217221
Column definitions mapping logical names (without prefix) to types.
218-
Supported: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``.
222+
Supported:
223+
- Primitive types: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``
224+
- Enum subclass (IntEnum preferred): generates a local option set.
225+
Optional multilingual labels via ``__labels__ = {1033: {"Active": "Active"}, 1036: {"Active": "Actif"}}``
219226
220227
Returns
221228
-------
@@ -298,5 +305,26 @@ def upload_file(
298305
)
299306
return None
300307

308+
# Cache utilities
309+
def flush_cache(self, kind) -> int:
310+
"""Flush cached client metadata/state.
311+
312+
Currently supported kinds:
313+
- 'picklist': clears entries from the picklist label cache used by label -> int conversion.
314+
315+
Parameters
316+
----------
317+
kind : str
318+
Cache kind to flush. Only 'picklist' is implemented today. Future kinds
319+
(e.g. 'entityset', 'primaryid') can be added without breaking the signature.
320+
321+
Returns
322+
-------
323+
int
324+
Number of cache entries removed.
325+
326+
"""
327+
return self._get_odata()._flush_cache(kind)
328+
301329
__all__ = ["DataverseClient"]
302330

0 commit comments

Comments
 (0)