It lives \o/
This commit is contained in:
parent
1ff21c7f29
commit
13dc426fc5
@ -6,5 +6,9 @@ class PhashPlatformRuntimeError(PhasmPlatformError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PhashPlatformNonIntMainReturnError(PhashPlatformRuntimeError):
|
class PhashPlatformServiceNotFound(PhashPlatformRuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PhashPlatformServiceMethodNotFound(PhashPlatformRuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -21,6 +21,9 @@ class MethodArgument:
|
|||||||
|
|
||||||
return self.name == other.name and self.value_type == other.value_type
|
return self.name == other.name and self.value_type == other.value_type
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'MethodArgument({repr(self.name)}, {repr(self.value_type)})'
|
||||||
|
|
||||||
|
|
||||||
class Method:
|
class Method:
|
||||||
__slots__ = ('name', 'args', 'return_type', )
|
__slots__ = ('name', 'args', 'return_type', )
|
||||||
@ -34,6 +37,9 @@ class Method:
|
|||||||
self.args = args
|
self.args = args
|
||||||
self.return_type = return_type
|
self.return_type = return_type
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'Method({repr(self.name)}, {repr(self.args)}, {repr(self.return_type)})'
|
||||||
|
|
||||||
|
|
||||||
class MethodCallError:
|
class MethodCallError:
|
||||||
pass
|
pass
|
||||||
@ -62,3 +68,6 @@ class MethodCall:
|
|||||||
self.args = args
|
self.args = args
|
||||||
self.on_success = on_success
|
self.on_success = on_success
|
||||||
self.on_error = on_error
|
self.on_error = on_error
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'MethodCall({repr(self.method)}, {repr(self.args)}, {repr(self.on_success)}, {repr(self.on_error)})'
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
class BaseRouter:
|
from .method import MethodCall
|
||||||
def post_message(self, namespace: bytes, topic: bytes, kind: bytes, body: bytes) -> None:
|
from .service import Service
|
||||||
|
|
||||||
|
|
||||||
|
class MethodCallRouterInterface:
|
||||||
|
def send_call(self, service: Service, call: MethodCall) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class StdOutRouter(BaseRouter):
|
|
||||||
def post_message(self, namespace: bytes, topic: bytes, kind: bytes, body: bytes) -> None:
|
|
||||||
print(f'ns={namespace.decode()},t={topic.decode()},k={kind.decode()} {body.decode()}')
|
|
||||||
|
|||||||
25
phasmplatform/common/service.py
Normal file
25
phasmplatform/common/service.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
|
from .method import Method
|
||||||
|
|
||||||
|
|
||||||
|
class Service:
|
||||||
|
__slots__ = ('name', 'methods', )
|
||||||
|
|
||||||
|
name: str
|
||||||
|
methods: Dict[str, Method]
|
||||||
|
|
||||||
|
def __init__(self, name: str, methods: List[Method]) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.methods = {
|
||||||
|
x.name: x
|
||||||
|
for x in methods
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'Service({repr(self.name)}, {repr(list(self.methods.values()))})'
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceDiscoveryInterface:
|
||||||
|
def find_service(self, name: str) -> Optional[Service]:
|
||||||
|
raise NotImplementedError
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
from .valuetype import ValueType
|
from .valuetype import ValueType, none
|
||||||
|
|
||||||
ValueData = Union[None, bytes]
|
ValueData = Union[None, bytes]
|
||||||
|
|
||||||
@ -19,4 +19,7 @@ class Value:
|
|||||||
return self.value_type is other.value_type and self.data == other.data
|
return self.value_type is other.value_type and self.data == other.data
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'Value(valuetype.{self.value_type.name}, {repr(self.data)})'
|
return f'Value({repr(self.value_type)}, {repr(self.data)})'
|
||||||
|
|
||||||
|
|
||||||
|
NoneValue = Value(none, None)
|
||||||
|
|||||||
@ -15,6 +15,9 @@ class ValueType:
|
|||||||
|
|
||||||
return self is other
|
return self is other
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'valuetype.{self.name}'
|
||||||
|
|
||||||
|
|
||||||
bytes = ValueType('bytes')
|
bytes = ValueType('bytes')
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@ -5,8 +7,10 @@ from queue import Empty, Queue
|
|||||||
|
|
||||||
from phasmplatform.common import valuetype
|
from phasmplatform.common import valuetype
|
||||||
from phasmplatform.common.config import from_toml
|
from phasmplatform.common.config import from_toml
|
||||||
from phasmplatform.common.method import Method, MethodCall
|
from phasmplatform.common.method import Method, MethodArgument, MethodCall
|
||||||
from phasmplatform.common.router import StdOutRouter
|
from phasmplatform.common.router import MethodCallRouterInterface
|
||||||
|
from phasmplatform.common.service import Service, ServiceDiscoveryInterface
|
||||||
|
from phasmplatform.common.value import NoneValue
|
||||||
|
|
||||||
from .runners.base import RunnerInterface
|
from .runners.base import RunnerInterface
|
||||||
from .runners.wasmtime import WasmTimeRunner
|
from .runners.wasmtime import WasmTimeRunner
|
||||||
@ -19,22 +23,92 @@ def runner_thread(runner: RunnerInterface, queue: Queue[MethodCall]) -> None:
|
|||||||
except Empty:
|
except Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
print('rt call', runner, queue, call)
|
||||||
runner.do_call(call)
|
runner.do_call(call)
|
||||||
|
|
||||||
|
|
||||||
|
def make_prelude() -> Service:
|
||||||
|
methods: List[Method] = []
|
||||||
|
|
||||||
|
methods.append(Method('log_bytes', [
|
||||||
|
MethodArgument('data', valuetype.bytes)
|
||||||
|
], valuetype.none))
|
||||||
|
|
||||||
|
return Service('prelude', methods)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalhostRunner(RunnerInterface):
|
||||||
|
def do_call(self, call: MethodCall) -> None:
|
||||||
|
if call.method.name == 'on_module_loaded':
|
||||||
|
print('LocalhostRunner loaded')
|
||||||
|
call.on_success(NoneValue)
|
||||||
|
return
|
||||||
|
|
||||||
|
if call.method.name == 'log_bytes':
|
||||||
|
print('LOG-BYTES:', repr(call.args[0].data))
|
||||||
|
call.on_success(NoneValue)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise NotImplementedError(call)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalhostServiceDiscovery(ServiceDiscoveryInterface):
|
||||||
|
services: Dict[str, Tuple[Service, Queue[MethodCall]]]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.services = {}
|
||||||
|
|
||||||
|
def register_service(self, service: Service, queue: Queue[MethodCall]) -> None:
|
||||||
|
self.services[service.name] = (service, queue, )
|
||||||
|
|
||||||
|
def find_service(self, name: str) -> Optional[Service]:
|
||||||
|
parts = self.services.get(name, None)
|
||||||
|
if parts is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return parts[0]
|
||||||
|
|
||||||
|
|
||||||
|
class LocalhostMethodCallRouter(MethodCallRouterInterface):
|
||||||
|
def __init__(self, service_discovery: LocalhostServiceDiscovery) -> None:
|
||||||
|
self.service_discovery = service_discovery
|
||||||
|
|
||||||
|
def send_call(self, service: Service, call: MethodCall) -> None:
|
||||||
|
assert service.name in self.service_discovery.services
|
||||||
|
queue = self.service_discovery.services[service.name][1]
|
||||||
|
print('send_call', service, call, queue)
|
||||||
|
queue.put(call)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
with open('config.toml', 'rb') as fil:
|
with open('config.toml', 'rb') as fil:
|
||||||
config = from_toml(fil)
|
config = from_toml(fil)
|
||||||
|
|
||||||
del config
|
del config
|
||||||
|
|
||||||
stdout_router = StdOutRouter()
|
localhost_queue: Queue[MethodCall] = Queue()
|
||||||
|
echo_client_queue: Queue[MethodCall] = Queue()
|
||||||
|
echo_server_queue: Queue[MethodCall] = Queue()
|
||||||
|
|
||||||
with open('/home/johan/projects/idea/phasm/examples/echoclient.wasm', 'rb') as fil:
|
service_discovery = LocalhostServiceDiscovery()
|
||||||
echo_client = WasmTimeRunner(stdout_router, fil.read())
|
method_call_router = LocalhostMethodCallRouter(service_discovery)
|
||||||
|
|
||||||
|
localhost = LocalhostRunner()
|
||||||
|
service_discovery.register_service(make_prelude(), localhost_queue)
|
||||||
|
|
||||||
with open('/home/johan/projects/idea/phasm/examples/echoserver.wasm', 'rb') as fil:
|
with open('/home/johan/projects/idea/phasm/examples/echoserver.wasm', 'rb') as fil:
|
||||||
echo_server = WasmTimeRunner(stdout_router, fil.read())
|
echo_server = WasmTimeRunner(service_discovery, method_call_router, fil.read())
|
||||||
|
service_discovery.register_service(Service('echoserver', [
|
||||||
|
Method('echo', [
|
||||||
|
MethodArgument('msg', valuetype.bytes)
|
||||||
|
], valuetype.bytes)
|
||||||
|
]), echo_server_queue)
|
||||||
|
|
||||||
|
with open('/home/johan/projects/idea/phasm/examples/echoclient.wasm', 'rb') as fil:
|
||||||
|
echo_client = WasmTimeRunner(service_discovery, method_call_router, fil.read())
|
||||||
|
|
||||||
|
# service_discovery.register_service(echo_client, echo_client_queue)
|
||||||
|
# service_discovery.register_service(echo_server, echo_server_queue)
|
||||||
|
|
||||||
on_module_loaded = MethodCall(
|
on_module_loaded = MethodCall(
|
||||||
Method('on_module_loaded', [], valuetype.none),
|
Method('on_module_loaded', [], valuetype.none),
|
||||||
@ -43,15 +117,15 @@ def main() -> int:
|
|||||||
lambda x: None, # TODO: Check for MethodNotFoundError, otherwise report it
|
lambda x: None, # TODO: Check for MethodNotFoundError, otherwise report it
|
||||||
)
|
)
|
||||||
|
|
||||||
echo_client_queue: Queue[MethodCall] = Queue()
|
localhost_queue.put(on_module_loaded)
|
||||||
echo_client_queue.put(on_module_loaded)
|
echo_client_queue.put(on_module_loaded)
|
||||||
|
|
||||||
echo_server_queue: Queue[MethodCall] = Queue()
|
|
||||||
echo_server_queue.put(on_module_loaded)
|
echo_server_queue.put(on_module_loaded)
|
||||||
|
|
||||||
|
localhost_thread = threading.Thread(target=runner_thread, args=(localhost, localhost_queue))
|
||||||
echo_client_thread = threading.Thread(target=runner_thread, args=(echo_client, echo_client_queue))
|
echo_client_thread = threading.Thread(target=runner_thread, args=(echo_client, echo_client_queue))
|
||||||
echo_server_thread = threading.Thread(target=runner_thread, args=(echo_server, echo_server_queue))
|
echo_server_thread = threading.Thread(target=runner_thread, args=(echo_server, echo_server_queue))
|
||||||
|
|
||||||
|
localhost_thread.start()
|
||||||
echo_client_thread.start()
|
echo_client_thread.start()
|
||||||
echo_server_thread.start()
|
echo_server_thread.start()
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +1,37 @@
|
|||||||
from typing import TextIO, Union
|
from typing import TextIO, Union
|
||||||
|
|
||||||
from phasmplatform.common import valuetype
|
from phasmplatform.common import valuetype
|
||||||
from phasmplatform.common.router import BaseRouter
|
|
||||||
from phasmplatform.common.method import MethodCall
|
from phasmplatform.common.method import MethodCall
|
||||||
|
from phasmplatform.common.router import MethodCallRouterInterface
|
||||||
|
from phasmplatform.common.service import ServiceDiscoveryInterface
|
||||||
from phasmplatform.common.value import Value
|
from phasmplatform.common.value import Value
|
||||||
from phasmplatform.common.valuetype import ValueType
|
from phasmplatform.common.valuetype import ValueType
|
||||||
|
|
||||||
|
|
||||||
|
WasmValue = Union[None, int, float]
|
||||||
|
|
||||||
|
|
||||||
class RunnerInterface:
|
class RunnerInterface:
|
||||||
__slots__ = ('router', )
|
__slots__ = ('router', )
|
||||||
|
|
||||||
def do_call(self, call: MethodCall) -> None:
|
def do_call(self, call: MethodCall) -> None:
|
||||||
|
"""
|
||||||
|
Executes the call on the current container
|
||||||
|
|
||||||
|
This method is responsible for calling the on_success or on_error method.
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class BaseRunner(RunnerInterface):
|
class BaseRunner(RunnerInterface):
|
||||||
__slots__ = ('router', )
|
__slots__ = ('service_discovery', 'method_call_router', )
|
||||||
|
|
||||||
router: BaseRouter
|
service_discovery: ServiceDiscoveryInterface
|
||||||
|
method_call_router: MethodCallRouterInterface
|
||||||
|
|
||||||
def __init__(self, router: BaseRouter) -> None:
|
def __init__(self, service_discovery: ServiceDiscoveryInterface, method_call_router: MethodCallRouterInterface) -> None:
|
||||||
self.router = router
|
self.service_discovery = service_discovery
|
||||||
|
self.method_call_router = method_call_router
|
||||||
|
|
||||||
def alloc_bytes(self, data: bytes) -> int:
|
def alloc_bytes(self, data: bytes) -> int:
|
||||||
"""
|
"""
|
||||||
@ -34,14 +45,18 @@ class BaseRunner(RunnerInterface):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def value_to_wasm(self, val: Value) -> Union[None, int, float]:
|
def value_to_wasm(self, val: Value) -> WasmValue:
|
||||||
|
if val.value_type is valuetype.none:
|
||||||
|
assert val.data is None # type hint
|
||||||
|
return None
|
||||||
|
|
||||||
if val.value_type is valuetype.bytes:
|
if val.value_type is valuetype.bytes:
|
||||||
assert isinstance(val.data, bytes) # type hint
|
assert isinstance(val.data, bytes) # type hint
|
||||||
return self.alloc_bytes(val.data)
|
return self.alloc_bytes(val.data)
|
||||||
|
|
||||||
raise NotImplementedError(val)
|
raise NotImplementedError(val)
|
||||||
|
|
||||||
def value_from_wasm(self, value_type: ValueType, val: Union[None, int, float]) -> Value:
|
def value_from_wasm(self, value_type: ValueType, val: WasmValue) -> Value:
|
||||||
if value_type is valuetype.none:
|
if value_type is valuetype.none:
|
||||||
assert val is None # type hint
|
assert val is None # type hint
|
||||||
return Value(value_type, None)
|
return Value(value_type, None)
|
||||||
|
|||||||
@ -1,40 +1,66 @@
|
|||||||
from typing import List
|
from typing import Any, List
|
||||||
|
|
||||||
import ctypes
|
import ctypes
|
||||||
|
import functools
|
||||||
import struct
|
import struct
|
||||||
|
from queue import Empty, Queue
|
||||||
|
|
||||||
import wasmtime
|
import wasmtime
|
||||||
|
|
||||||
from phasmplatform.common.method import MethodCall, MethodNotFoundError
|
from phasmplatform.common import valuetype
|
||||||
from phasmplatform.common.router import BaseRouter
|
from phasmplatform.common.exceptions import PhashPlatformServiceNotFound, PhashPlatformServiceMethodNotFound
|
||||||
from .base import BaseRunner
|
from phasmplatform.common.method import Method, MethodCall, MethodCallError, MethodNotFoundError
|
||||||
|
from phasmplatform.common.router import MethodCallRouterInterface
|
||||||
|
from phasmplatform.common.service import Service, ServiceDiscoveryInterface
|
||||||
|
from phasmplatform.common.value import Value
|
||||||
|
from phasmplatform.common.valuetype import ValueType
|
||||||
|
|
||||||
|
from .base import BaseRunner, WasmValue
|
||||||
|
|
||||||
|
|
||||||
class WasmTimeRunner(BaseRunner):
|
class WasmTimeRunner(BaseRunner):
|
||||||
__slots__ = ('store', 'module', 'instance', 'exports')
|
__slots__ = ('store', 'module', 'instance', 'exports')
|
||||||
|
|
||||||
def __init__(self, router: BaseRouter, wasm_bin: bytes) -> None:
|
def __init__(
|
||||||
super().__init__(router)
|
self,
|
||||||
|
service_discovery: ServiceDiscoveryInterface,
|
||||||
|
method_call_router: MethodCallRouterInterface,
|
||||||
|
wasm_bin: bytes,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(service_discovery, method_call_router)
|
||||||
|
|
||||||
self.store = wasmtime.Store()
|
self.store = wasmtime.Store()
|
||||||
|
|
||||||
possible_imports = {
|
|
||||||
'prelude': {
|
|
||||||
'log_bytes': wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], []), self.log_bytes),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.module = wasmtime.Module(self.store.engine, wasm_bin)
|
self.module = wasmtime.Module(self.store.engine, wasm_bin)
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
def dump_args(*args: Any, **kwargs: Any) -> None:
|
||||||
|
print('args', args)
|
||||||
|
print('kwargs', kwargs)
|
||||||
|
|
||||||
imports: List[wasmtime.Func] = []
|
imports: List[wasmtime.Func] = []
|
||||||
for imprt in self.module.imports:
|
for imprt in self.module.imports:
|
||||||
if imprt.module not in possible_imports:
|
service = service_discovery.find_service(imprt.module)
|
||||||
raise Exception('Service not found')
|
if service is None:
|
||||||
|
raise PhashPlatformServiceNotFound(
|
||||||
|
f'Dependent service "{imprt.module}" not found; could not provide "{imprt.name}"'
|
||||||
|
)
|
||||||
|
|
||||||
if imprt.name not in possible_imports[imprt.module]:
|
assert imprt.name is not None # type hint
|
||||||
raise Exception('Method not found in service')
|
|
||||||
|
|
||||||
imports.append(possible_imports[imprt.module][imprt.name])
|
method = service.methods.get(imprt.name)
|
||||||
|
if method is None:
|
||||||
|
raise PhashPlatformServiceMethodNotFound(
|
||||||
|
f'Dependent service "{imprt.module}" found, but it does not provide "{imprt.name}"'
|
||||||
|
)
|
||||||
|
|
||||||
|
func = wasmtime.Func(
|
||||||
|
self.store,
|
||||||
|
build_func_type(method),
|
||||||
|
functools.partial(self.send_service_call, service, method)
|
||||||
|
)
|
||||||
|
|
||||||
|
imports.append(func)
|
||||||
|
|
||||||
self.instance = wasmtime.Instance(self.store, self.module, imports)
|
self.instance = wasmtime.Instance(self.store, self.module, imports)
|
||||||
|
|
||||||
@ -87,3 +113,65 @@ class WasmTimeRunner(BaseRunner):
|
|||||||
result = wasm_method(self.store, *act_args)
|
result = wasm_method(self.store, *act_args)
|
||||||
assert result is None or isinstance(result, (int, float, )) # type hint
|
assert result is None or isinstance(result, (int, float, )) # type hint
|
||||||
call.on_success(self.value_from_wasm(call.method.return_type, result))
|
call.on_success(self.value_from_wasm(call.method.return_type, result))
|
||||||
|
|
||||||
|
def send_service_call(self, service: Service, method: Method, *args: Any) -> WasmValue:
|
||||||
|
assert len(method.args) == len(args) # type hint
|
||||||
|
|
||||||
|
call_args = [
|
||||||
|
self.value_from_wasm(x.value_type, y)
|
||||||
|
for x, y in zip(method.args, args)
|
||||||
|
]
|
||||||
|
|
||||||
|
queue: Queue[Value] = Queue(maxsize=1)
|
||||||
|
|
||||||
|
def on_success(val: Value) -> None:
|
||||||
|
print('hi mom')
|
||||||
|
queue.put(val)
|
||||||
|
|
||||||
|
def on_error(err: MethodCallError) -> None:
|
||||||
|
print('Error while calling', service, method, args)
|
||||||
|
|
||||||
|
print('on_success', on_success)
|
||||||
|
call = MethodCall(method, call_args, on_success, on_error)
|
||||||
|
|
||||||
|
print(
|
||||||
|
'send_service_call',
|
||||||
|
'from-service=?', 'from-method=?', # TODO
|
||||||
|
f'to-service={service.name}', f'to-method={method.name}',
|
||||||
|
*args,
|
||||||
|
)
|
||||||
|
self.method_call_router.send_call(service, call)
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = queue.get(block=True, timeout=10)
|
||||||
|
except Empty:
|
||||||
|
print(
|
||||||
|
'send_service_call',
|
||||||
|
'from-service=?', 'from-method=?', # TODO
|
||||||
|
f'to-service={service.name}', f'to-method={method.name}',
|
||||||
|
'TIMEOUT',
|
||||||
|
)
|
||||||
|
raise Exception() # TODO
|
||||||
|
|
||||||
|
return self.value_to_wasm(value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_func_type(method: Method) -> wasmtime.FuncType:
|
||||||
|
if method.return_type is valuetype.none:
|
||||||
|
returns = []
|
||||||
|
else:
|
||||||
|
returns = [build_wasm_type(method.return_type)]
|
||||||
|
|
||||||
|
args = []
|
||||||
|
for arg in method.args:
|
||||||
|
assert arg.value_type is not valuetype.none # type hint
|
||||||
|
args.append(build_wasm_type(arg.value_type))
|
||||||
|
|
||||||
|
return wasmtime.FuncType(args, returns)
|
||||||
|
|
||||||
|
|
||||||
|
def build_wasm_type(value_type: ValueType) -> wasmtime.ValType:
|
||||||
|
if value_type is valuetype.bytes:
|
||||||
|
return wasmtime.ValType.i32() # Bytes are passed as pointer
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user