Skip to content

Commit 42b1a92

Browse files
Features (#2053)
* WIP * new api * /features endpoint * New backend API * Switch to packages in the UI * Basic UI support for features * typo * UI WIP * Unified markdown handling * slight fixes * Properly throw server errors * Better error messages * Basic feature checking * Added spinner while refreshing
1 parent c4c1370 commit 42b1a92

File tree

39 files changed

+1127
-637
lines changed

39 files changed

+1127
-637
lines changed

backend/src/api.py

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
import importlib
44
import os
55
from dataclasses import dataclass, field
6-
from typing import Callable, Dict, Iterable, List, Tuple, TypedDict, TypeVar
6+
from typing import (
7+
Awaitable,
8+
Callable,
9+
Dict,
10+
Iterable,
11+
List,
12+
NewType,
13+
Tuple,
14+
TypedDict,
15+
TypeVar,
16+
)
717

818
from sanic.log import logger
919

@@ -83,11 +93,13 @@ class NodeData:
8393
side_effects: bool
8494
deprecated: bool
8595
default_nodes: List[DefaultNode] | None # For iterators only
96+
features: List[FeatureId]
8697

8798
run: RunFn
8899

89100

90101
T = TypeVar("T", bound=RunFn)
102+
S = TypeVar("S")
91103

92104

93105
@dataclass
@@ -114,14 +126,20 @@ def register(
114126
default_nodes: List[DefaultNode] | None = None,
115127
decorators: List[Callable] | None = None,
116128
see_also: List[str] | str | None = None,
129+
features: List[FeatureId] | FeatureId | None = None,
117130
):
118131
if not isinstance(description, str):
119132
description = "\n\n".join(description)
120133

121-
if see_also is None:
122-
see_also = []
123-
if isinstance(see_also, str):
124-
see_also = [see_also]
134+
def to_list(x: List[S] | S | None) -> List[S]:
135+
if x is None:
136+
return []
137+
if isinstance(x, list):
138+
return x
139+
return [x]
140+
141+
see_also = to_list(see_also)
142+
features = to_list(features)
125143

126144
def run_check(level: CheckLevel, run: Callable[[bool], None]):
127145
if level == CheckLevel.NONE:
@@ -170,6 +188,7 @@ def inner_wrapper(wrapped_func: T) -> T:
170188
side_effects=side_effects,
171189
deprecated=deprecated,
172190
default_nodes=default_nodes,
191+
features=features,
173192
run=wrapped_func,
174193
)
175194

@@ -226,13 +245,59 @@ def toDict(self):
226245
}
227246

228247

248+
FeatureId = NewType("FeatureId", str)
249+
250+
251+
@dataclass
252+
class Feature:
253+
id: str
254+
name: str
255+
description: str
256+
behavior: FeatureBehavior | None = None
257+
258+
def add_behavior(self, check: Callable[[], Awaitable[FeatureState]]) -> FeatureId:
259+
if self.behavior is not None:
260+
raise ValueError("Behavior already set")
261+
262+
self.behavior = FeatureBehavior(check=check)
263+
return FeatureId(self.id)
264+
265+
def toDict(self):
266+
return {
267+
"id": self.id,
268+
"name": self.name,
269+
"description": self.description,
270+
}
271+
272+
273+
@dataclass
274+
class FeatureBehavior:
275+
check: Callable[[], Awaitable[FeatureState]]
276+
277+
278+
@dataclass(frozen=True)
279+
class FeatureState:
280+
is_enabled: bool
281+
details: str | None = None
282+
283+
@staticmethod
284+
def enabled(details: str | None = None) -> "FeatureState":
285+
return FeatureState(is_enabled=True, details=details)
286+
287+
@staticmethod
288+
def disabled(details: str | None = None) -> "FeatureState":
289+
return FeatureState(is_enabled=False, details=details)
290+
291+
229292
@dataclass
230293
class Package:
231294
where: str
295+
id: str
232296
name: str
233297
description: str
234298
dependencies: List[Dependency] = field(default_factory=list)
235299
categories: List[Category] = field(default_factory=list)
300+
features: List[Feature] = field(default_factory=list)
236301

237302
def add_category(
238303
self,
@@ -259,6 +324,19 @@ def add_dependency(
259324
):
260325
self.dependencies.append(dependency)
261326

327+
def add_feature(
328+
self,
329+
id: str, # pylint: disable=redefined-builtin
330+
name: str,
331+
description: str,
332+
) -> Feature:
333+
if any(f.id == id for f in self.features):
334+
raise ValueError(f"Duplicate feature id: {id}")
335+
336+
feature = Feature(id=id, name=name, description=description)
337+
self.features.append(feature)
338+
return feature
339+
262340

263341
def _iter_py_files(directory: str):
264342
for root, _, files in os.walk(directory):
@@ -331,6 +409,18 @@ def _refresh_nodes(self):
331409

332410

333411
def add_package(
334-
where: str, name: str, description: str, dependencies: List[Dependency]
412+
where: str,
413+
id: str, # pylint: disable=redefined-builtin
414+
name: str,
415+
description: str,
416+
dependencies: List[Dependency] | None = None,
335417
) -> Package:
336-
return registry.add(Package(where, name, description, dependencies))
418+
return registry.add(
419+
Package(
420+
where=where,
421+
id=id,
422+
name=name,
423+
description=description,
424+
dependencies=dependencies or [],
425+
)
426+
)

0 commit comments

Comments
 (0)