Skip to content

Commit 9cb27b6

Browse files
author
Max Wang
committed
remove feature flag
1 parent 1624cf7 commit 9cb27b6

5 files changed

Lines changed: 5 additions & 240 deletions

File tree

README.md

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -335,45 +335,6 @@ VS Code Tasks
335335
- Install deps: `Install deps (pip)`
336336
- Run example: `Run Quickstart (Dataverse SDK)`
337337

338-
339-
## Feature Flags (extensible) & Picklist Label Coercion
340-
341-
The SDK uses a lightweight feature flag system.
342-
343-
Source & schema:
344-
`feature_flags.json` (packaged) provides defaults. Each feature MUST use the strict object form only:
345-
346-
```json
347-
{
348-
"flag_name": {
349-
"default": false,
350-
"description": "Human readable explanation of the flag purpose"
351-
}
352-
}
353-
```
354-
355-
Notes:
356-
- Both `default` (bool) and `description` (non-empty string) are required.
357-
- No extra keys are allowed (startup fails fast on unknown keys).
358-
- The JSON file does NOT accept boolean shorthand; that form is permitted only for runtime overrides passed to the client.
359-
360-
Override a feature flag mapping at construction:
361-
```python
362-
client = DataverseClient(
363-
base_url="https://yourorg.crm.dynamics.com",
364-
feature_flags={
365-
"option_set_label_conversion": True,
366-
}
367-
)
368-
```
369-
370-
Toggle/check at runtime:
371-
```python
372-
client.enable_feature("option_set_label_conversion")
373-
client.is_feature_enabled("option_set_label_conversion")
374-
client.disable_feature("option_set_label_conversion")
375-
```
376-
377338
## Limitations / Future Work
378339
- No general-purpose OData batching, upsert, or association operations yet.
379340
- `DeleteMultiple` not yet exposed.

src/dataverse_sdk/client.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,13 @@ def __init__(
4242
base_url: str,
4343
credential: Optional[TokenCredential] = None,
4444
config: Optional[DataverseConfig] = None,
45-
feature_flags: Optional[Dict[str, bool]] = None,
4645
) -> None:
4746
self.auth = AuthManager(credential)
4847
self._base_url = (base_url or "").rstrip("/")
4948
if not self._base_url:
5049
raise ValueError("base_url is required.")
5150
self._config = config or DataverseConfig.from_env()
5251
self._odata: Optional[ODataClient] = None
53-
self._feature_flags = dict(feature_flags) if isinstance(feature_flags, dict) else None
5452

5553
def _get_odata(self) -> ODataClient:
5654
"""Get or create the internal OData client instance.
@@ -65,7 +63,6 @@ def _get_odata(self) -> ODataClient:
6563
self.auth,
6664
self._base_url,
6765
self._config,
68-
feature_flags=self._feature_flags,
6966
)
7067
return self._odata
7168

@@ -276,18 +273,5 @@ def flush_cache(self, kind) -> int:
276273
"""
277274
return self._get_odata()._flush_cache(kind)
278275

279-
# ---------------------- Feature flags / toggles ----------------------
280-
def set_feature(self, name: str, enabled: bool) -> None:
281-
self._get_odata().set_feature(name, enabled)
282-
283-
def enable_feature(self, name: str) -> None:
284-
self.set_feature(name, True)
285-
286-
def disable_feature(self, name: str) -> None:
287-
self.set_feature(name, False)
288-
289-
def is_feature_enabled(self, name: str) -> bool:
290-
return self._get_odata().is_feature_enabled(name)
291-
292276
__all__ = ["DataverseClient"]
293277

src/dataverse_sdk/feature_flags.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/dataverse_sdk/odata.py

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ def __init__(
2727
auth,
2828
base_url: str,
2929
config=None,
30-
feature_flags: Optional[Dict[str, bool]] = None,
3130
) -> None:
3231
self.auth = auth
3332
self.base_url = (base_url or "").rstrip("/")
@@ -51,72 +50,6 @@ def __init__(
5150
# Picklist label cache: (logical_name, attribute_logical) -> {'map': {...}, 'ts': epoch_seconds}
5251
self._picklist_label_cache = {}
5352
self._picklist_cache_ttl_seconds = 3600 # 1 hour TTL
54-
# Load required feature flags from bundled JSON resource; fail hard if missing or invalid.
55-
try:
56-
data = ir.files("dataverse_sdk").joinpath("feature_flags.json").read_text(encoding="utf-8")
57-
except Exception as e:
58-
raise RuntimeError(f"Failed to load feature_flags.json resource: {e}") from e
59-
try:
60-
loaded = json.loads(data) if data else {}
61-
except Exception as e:
62-
raise RuntimeError(f"feature_flags.json is not valid JSON: {e}") from e
63-
if not isinstance(loaded, dict):
64-
raise RuntimeError("feature_flags.json root must be a JSON object mapping feature -> (bool | object)")
65-
66-
self._features: Dict[str, bool] = {}
67-
self._feature_metadata: Dict[str, Dict[str, Any]] = {}
68-
69-
for raw_key, raw_val in loaded.items():
70-
# Enforce key type and non-empty constraint
71-
if not isinstance(raw_key, str):
72-
raise RuntimeError(f"feature_flags.json key '{raw_key}' is not a string")
73-
key = raw_key.strip().lower()
74-
if not key:
75-
raise RuntimeError("feature_flags.json contains an empty feature name")
76-
# Object form with strict schema: { "default": bool, "description": str }
77-
if isinstance(raw_val, dict):
78-
required_keys = {"default", "description"}
79-
unknown = set(raw_val.keys()) - required_keys
80-
if unknown:
81-
raise RuntimeError(
82-
f"Feature '{raw_key}' has unknown metadata keys: {', '.join(sorted(unknown))}. Allowed: default, description"
83-
)
84-
missing = required_keys - set(raw_val.keys())
85-
if missing:
86-
raise RuntimeError(
87-
f"Feature '{raw_key}' object missing required key(s): {', '.join(sorted(missing))} (requires: default, description)"
88-
)
89-
if not isinstance(raw_val["default"], bool):
90-
raise RuntimeError(f"Feature '{raw_key}' field 'default' must be boolean")
91-
desc = raw_val["description"]
92-
if not isinstance(desc, str) or not desc.strip():
93-
raise RuntimeError(f"Feature '{raw_key}' field 'description' must be a non-empty string")
94-
self._features[key] = raw_val["default"]
95-
self._feature_metadata[key] = {"description": desc.strip()}
96-
continue
97-
# Any other type is invalid
98-
raise RuntimeError(
99-
f"Feature '{raw_key}' must be an object with 'default' (bool) and required 'description' (non-empty str)"
100-
)
101-
102-
# Overlay user overrides (if supplied). Overrides must:
103-
# - use existing feature names declared in feature_flags.json
104-
# - provide boolean values only (no coercion of truthy/falsy non-bools)
105-
# - use non-empty string keys
106-
if isinstance(feature_flags, dict):
107-
for k, v in feature_flags.items():
108-
if not isinstance(k, str) or not k.strip():
109-
raise ValueError("feature_flags override keys must be non-empty strings")
110-
norm = k.strip().lower()
111-
if norm not in self._features:
112-
raise ValueError(
113-
f"Unknown feature flag override '{k}' (not declared in feature_flags.json)"
114-
)
115-
if not isinstance(v, bool):
116-
raise ValueError(
117-
f"Override value for feature '{k}' must be boolean (got {type(v).__name__})"
118-
)
119-
self._features[norm] = v
12053

12154
def _headers(self) -> Dict[str, str]:
12255
"""Build standard OData headers with bearer auth."""
@@ -845,6 +778,10 @@ def _optionset_map(self, entity_set: str, attr_logical: str) -> Optional[Dict[st
845778
846779
Returns empty dict if attribute is not a picklist or has no options. Returns None only
847780
for invalid inputs or unexpected metadata parse failures.
781+
782+
Notes
783+
-----
784+
- This method calls the Web API twice per attribute so it could have perf impact when there are lots of columns on the entity.
848785
"""
849786
if not entity_set or not attr_logical:
850787
return None
@@ -944,9 +881,6 @@ def _convert_labels_to_ints(self, entity_set: str, record: Dict[str, Any]) -> Di
944881
Heuristic: For each string value, attempt to resolve against picklist metadata.
945882
If attribute isn't a picklist or label not found, value left unchanged.
946883
"""
947-
# Fast-path: feature disabled (default). Return original record without copy to avoid overhead.
948-
if not self.is_feature_enabled("option_set_label_conversion"):
949-
return record
950884
out = record.copy()
951885
for k, v in list(out.items()):
952886
if not isinstance(v, str) or not v.strip():
@@ -1150,21 +1084,4 @@ def _flush_cache(
11501084

11511085
removed = len(self._picklist_label_cache)
11521086
self._picklist_label_cache.clear()
1153-
return removed
1154-
1155-
# ---------------------- Feature flags / toggles --------------------
1156-
def set_feature(self, name: str, enabled: bool) -> None:
1157-
if not isinstance(name, str) or not name.strip():
1158-
raise ValueError("Feature name must be a non-empty string")
1159-
self._features[name.strip().lower()] = bool(enabled)
1160-
1161-
def enable_feature(self, name: str) -> None:
1162-
self.set_feature(name, True)
1163-
1164-
def disable_feature(self, name: str) -> None:
1165-
self.set_feature(name, False)
1166-
1167-
def is_feature_enabled(self, name: str) -> bool:
1168-
if not isinstance(name, str):
1169-
return False
1170-
return bool(self._features.get(name.strip().lower()))
1087+
return removed

tests/test_feature_flags.py

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)