Skip to content

Commit 3b57714

Browse files
committed
💥 refactor Node
1 parent adf06f1 commit 3b57714

File tree

14 files changed

+588
-286
lines changed

14 files changed

+588
-286
lines changed

README.md

+21-10
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,29 @@ WIP
1414
## Example
1515

1616
```python
17-
from arclet.cithun import User, NodeState, Group, context
17+
from arclet.cithun import SyncMonitor, Node, NodeState, context, PermissionExecutor
18+
19+
monitor = SyncMonitor()
20+
21+
baz = Node("/foo/bar/baz").mkdir(parents=True)
22+
qux = (baz / "qux").touch()
1823

1924
with context(scope="main"):
20-
admin = Group('admin', 100)
21-
admin.add("/foo/bar/baz/", NodeState("vma"))
25+
admin = monitor.new_group('admin', 100)
26+
PermissionExecutor.root.set(admin, baz, NodeState("vma"))
2227

23-
user = User('cithun')
24-
user.join(admin)
28+
user = monitor.new_user('cithun')
29+
monitor.user_inherit(user, admin)
2530

26-
user.get("/foo/bar/baz/") # vma
27-
user.sadd("/foo/bar/baz/qux")
28-
user.available("/foo/bar/baz/qux") # False as default perm of qux is vm-
29-
admin.smodify("/foo/bar/baz/", NodeState("v-a"))
30-
user.modify("/foo/bar/baz/qux", NodeState(7)) # False as /baz/ is not modifiable
31+
assert PermissionExecutor.root.get(user, baz).most == NodeState("vma")
32+
assert not PermissionExecutor.root.get(user, qux).most.available
33+
34+
PermissionExecutor.root.set(user, qux, NodeState(7))
35+
assert PermissionExecutor.root.get(user, qux).most.available
36+
37+
PermissionExecutor.root.set(admin, baz, NodeState("v-a"))
38+
try:
39+
PermissionExecutor(user).set(user, qux, NodeState("vm-"))
40+
except PermissionError as e:
41+
print(e) # Permission denied as /baz/ is not modifiable
3142
```

src/arclet/cithun/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from . import monitor as monitor
1+
from .function import PermissionExecutor as PermissionExecutor
22
from .ctx import Context as Context
33
from .ctx import context as context
4+
from .monitor import AsyncMonitor as AsyncMonitor
5+
from .monitor import SyncMonitor as SyncMonitor
46
from .node import ROOT as ROOT
57
from .node import Node as Node
68
from .node import NodeState as NodeState

src/arclet/cithun/builtins/__init__.py

Whitespace-only changes.

src/arclet/cithun/builtins/monitor.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
from contextlib import contextmanager
3+
from pathlib import Path
4+
from weakref import WeakValueDictionary
5+
6+
from arclet.cithun import Group, User
7+
from arclet.cithun.monitor import SyncMonitor
8+
9+
from .owner import DefaultGroup, DefaultUser
10+
11+
12+
class DefaultMonitor(SyncMonitor):
13+
def __init__(self, file: Path):
14+
if not file.suffix.startswith(".json"):
15+
raise ValueError(file)
16+
self.file = file
17+
self.USER_TABLE = WeakValueDictionary()
18+
self.GROUP_TABLE = WeakValueDictionary()
19+
20+
def new_group(self, name: str, priority: int):
21+
if name in self.GROUP_TABLE:
22+
raise ValueError(f"Group {name} already exists")
23+
group = DefaultGroup(name, priority)
24+
self.GROUP_TABLE[name] = group
25+
return group
26+
27+
def new_user(self, name: str):
28+
if name in self.USER_TABLE:
29+
raise ValueError(f"User {name} already exists")
30+
user = DefaultUser(name)
31+
self.USER_TABLE[name] = user
32+
return user
33+
34+
def load(self):
35+
if self.file.exists():
36+
with self.file.open("r", encoding="utf-8") as f:
37+
data = json.load(f)
38+
users = {name: DefaultUser.parse(raw) for name, raw in data["users"].items()}
39+
groups = {name: DefaultGroup.parse(raw) for name, raw in data["groups"].items()}
40+
self.USER_TABLE.update(users)
41+
self.GROUP_TABLE.update(groups)
42+
for group in groups.values():
43+
group.inherits = [self.GROUP_TABLE[gp.name] for gp in group.inherits]
44+
for user in users.values():
45+
user.inherits = [self.USER_TABLE[gp.name] for gp in user.inherits]
46+
del users, groups
47+
else:
48+
with self.file.open("w+", encoding="utf-8") as f:
49+
json.dump({}, f, ensure_ascii=False)
50+
51+
def save(self):
52+
data = {
53+
"users": {user.name: user.dump() for user in self.USER_TABLE.values()},
54+
"groups": {group.name: group.dump() for group in self.GROUP_TABLE.values()},
55+
}
56+
with self.file.open("w", encoding="utf-8") as f:
57+
json.dump(data, f, ensure_ascii=False)
58+
59+
def group_inherit(self, target: Group, *groups: Group):
60+
for group in groups:
61+
if group not in target.inherits:
62+
target.inherits.append(group)
63+
64+
def user_inherit(self, target: User, *groups: Group):
65+
for group in groups:
66+
if group not in target.inherits:
67+
target.inherits.append(group)
68+
69+
def user_leave(self, target: User, group: Group):
70+
if group in target.inherits:
71+
target.inherits.remove(group)
72+
73+
@contextmanager
74+
def transaction(self):
75+
yield
76+
self.save()

src/arclet/cithun/builtins/owner.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
5+
from arclet.cithun.ctx import Context
6+
from arclet.cithun.node import Node, NodeState
7+
8+
9+
@dataclass(eq=True, unsafe_hash=True)
10+
class DefaultGroup:
11+
name: str
12+
priority: int
13+
nodes: dict[Node, dict[Context, NodeState]] = field(default_factory=dict, compare=False, hash=False)
14+
inherits: list = field(default_factory=list, compare=False, hash=False)
15+
16+
def dump(self):
17+
return {
18+
"name": self.name,
19+
"priority": self.priority,
20+
"nodes": {
21+
str(node): {str(ctx): state.state for ctx, state in data.items()} for node, data in self.nodes.items()
22+
},
23+
"inherits": [gp.name for gp in self.inherits],
24+
}
25+
26+
@classmethod
27+
def parse(cls, raw: dict):
28+
obj = cls(raw["name"], raw["priority"])
29+
obj.nodes = {
30+
Node(node): {Context.from_string(ctx): NodeState(state) for ctx, state in data.items()}
31+
for node, data in raw["nodes"].items()
32+
}
33+
obj.inherits = [DefaultGroup(name, 0) for name in raw["inherits"]]
34+
return obj
35+
36+
def __str__(self):
37+
return f"Group({self.name})"
38+
39+
40+
@dataclass(eq=True, unsafe_hash=True)
41+
class DefaultUser:
42+
name: str
43+
nodes: dict[Node, dict[Context, NodeState]] = field(default_factory=dict, compare=False, hash=False)
44+
inherits: list = field(default_factory=list, compare=False, hash=False)
45+
46+
def dump(self):
47+
return {
48+
"name": self.name,
49+
"nodes": {
50+
str(node): {str(ctx): state.state for ctx, state in data.items()} for node, data in self.nodes.items()
51+
},
52+
"inherits": [user.name for user in self.inherits],
53+
}
54+
55+
@classmethod
56+
def parse(cls, raw: dict):
57+
obj = cls(raw["name"])
58+
obj.nodes = {
59+
Node(node): {Context.from_string(ctx): NodeState(state) for ctx, state in data.items()}
60+
for node, data in raw["nodes"].items()
61+
}
62+
obj.inherits = [DefaultUser(name) for name in raw["inherits"]]
63+
return obj
64+
65+
def __str__(self):
66+
return f"User({self.name})"

src/arclet/cithun/ctx.py

+42-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections import UserDict
22
from contextlib import contextmanager
33
from contextvars import ContextVar
4-
from typing import Literal
4+
from typing import Callable, Dict, Generic, TypeVar
55

66

77
class Context(UserDict):
@@ -29,19 +29,6 @@ def from_string(cls, data: str):
2929
_data[segs[0]] = segs[1]
3030
return cls(**_data)
3131

32-
def satisfied(self, other: "Context", mode: Literal["least_one", "all"] = "least_one") -> bool:
33-
if not self.data:
34-
return True
35-
if not other.data:
36-
return True
37-
return self.contain_all(other) if mode == "all" else self.contain_least(other)
38-
39-
def contain_all(self, other: "Context"):
40-
return self.data == other.data
41-
42-
def contain_least(self, other: "Context"):
43-
return any((other.get(k) == v for k, v in self.data.items()))
44-
4532

4633
_ctx = ContextVar("context")
4734

@@ -53,3 +40,44 @@ def context(**kwargs):
5340
yield
5441
finally:
5542
_ctx.reset(token)
43+
44+
45+
class Satisfier:
46+
@staticmethod
47+
def all():
48+
return Satisfier(lambda self, other: self.data == other.data)
49+
50+
@staticmethod
51+
def least():
52+
return Satisfier(lambda self, other: any((other.get(k) == v for k, v in self.data.items())))
53+
54+
def __init__(self, func: Callable[[Context, Context], bool]):
55+
self.func = func
56+
57+
def __call__(self, target: "Context", other: "Context") -> bool:
58+
if not target.data:
59+
return True
60+
if not other.data:
61+
return True
62+
return self.func(target, other)
63+
64+
65+
T = TypeVar("T")
66+
67+
68+
class Result(Generic[T]):
69+
def __init__(self, data: Dict[Context, T], origin: Context):
70+
self.data = data
71+
self.origin = origin
72+
73+
@property
74+
def most(self) -> T:
75+
if self.origin in self.data:
76+
return self.data[self.origin]
77+
sortd = sorted(
78+
self.data.keys(), key=lambda x: sum((x.get(k) == v for k, v in self.origin.items())), reverse=True
79+
)
80+
return self.data[sortd[0]]
81+
82+
def __repr__(self):
83+
return f"Result({self.data})"

0 commit comments

Comments
 (0)