lapis-lazuli 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lapis_lazuli-0.2.0/.gitignore +22 -0
- lapis_lazuli-0.2.0/PKG-INFO +25 -0
- lapis_lazuli-0.2.0/README.md +15 -0
- lapis_lazuli-0.2.0/pyproject.toml +20 -0
- lapis_lazuli-0.2.0/src/lapis_lazuli/__init__.py +15 -0
- lapis_lazuli-0.2.0/src/lapis_lazuli/_plugin.py +50 -0
- lapis_lazuli-0.2.0/src/lapis_lazuli/_runtime.py +718 -0
- lapis_lazuli-0.2.0/src/lapis_lazuli/py.typed +1 -0
- lapis_lazuli-0.2.0/tests/test_sdk.py +265 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
.gradle/
|
|
3
|
+
.gradle-local/
|
|
4
|
+
.kotlin/
|
|
5
|
+
.idea/
|
|
6
|
+
.lapis/
|
|
7
|
+
.vscode/
|
|
8
|
+
*.iml
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.pyc
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
**/bin/
|
|
17
|
+
build/
|
|
18
|
+
dist/
|
|
19
|
+
node_modules/
|
|
20
|
+
out/
|
|
21
|
+
*.class
|
|
22
|
+
*.log
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lapis-lazuli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for Lapis Lazuli plugins
|
|
5
|
+
Author: Lapis Lazuli
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# lapis-lazuli
|
|
12
|
+
|
|
13
|
+
Python SDK for authoring Lapis Lazuli plugins with a Python-first surface.
|
|
14
|
+
|
|
15
|
+
Install:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
python -m pip install lapis-lazuli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Import:
|
|
22
|
+
|
|
23
|
+
```py
|
|
24
|
+
from lapis_lazuli import Plugin
|
|
25
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lapis-lazuli"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python SDK for Lapis Lazuli plugins"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Lapis Lazuli" },
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/lapis_lazuli"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ._plugin import Plugin, define_plugin
|
|
2
|
+
from ._runtime import GuestProxy, HttpResponse, Location, PluginContext, TitleOptions, wrap
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"GuestProxy",
|
|
6
|
+
"HttpResponse",
|
|
7
|
+
"Location",
|
|
8
|
+
"Plugin",
|
|
9
|
+
"PluginContext",
|
|
10
|
+
"TitleOptions",
|
|
11
|
+
"define_plugin",
|
|
12
|
+
"wrap",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._runtime import PluginContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Plugin:
|
|
10
|
+
def __init__(self, name: str, version: str | None = None) -> None:
|
|
11
|
+
self.name = name
|
|
12
|
+
self.version = version
|
|
13
|
+
self._startup_handler: Callable[[PluginContext], Any] | None = None
|
|
14
|
+
self._shutdown_handler: Callable[[PluginContext], Any] | None = None
|
|
15
|
+
|
|
16
|
+
def startup(self, handler: Callable[[PluginContext], Any]) -> Callable[[PluginContext], Any]:
|
|
17
|
+
self._startup_handler = handler
|
|
18
|
+
return handler
|
|
19
|
+
|
|
20
|
+
def shutdown(self, handler: Callable[[PluginContext], Any]) -> Callable[[PluginContext], Any]:
|
|
21
|
+
self._shutdown_handler = handler
|
|
22
|
+
return handler
|
|
23
|
+
|
|
24
|
+
def on_enable(self, raw_context: Any) -> Any:
|
|
25
|
+
if self._startup_handler is None:
|
|
26
|
+
return None
|
|
27
|
+
return self._startup_handler(PluginContext(raw_context))
|
|
28
|
+
|
|
29
|
+
def on_disable(self, raw_context: Any) -> Any:
|
|
30
|
+
if self._shutdown_handler is None:
|
|
31
|
+
return None
|
|
32
|
+
return self._shutdown_handler(PluginContext(raw_context))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def define_plugin(
|
|
36
|
+
*,
|
|
37
|
+
name: str,
|
|
38
|
+
version: str | None = None,
|
|
39
|
+
on_enable: Callable[[PluginContext], Any] | None = None,
|
|
40
|
+
on_disable: Callable[[PluginContext], Any] | None = None,
|
|
41
|
+
) -> Plugin:
|
|
42
|
+
plugin = Plugin(name=name, version=version)
|
|
43
|
+
|
|
44
|
+
if on_enable is not None:
|
|
45
|
+
plugin.startup(on_enable)
|
|
46
|
+
|
|
47
|
+
if on_disable is not None:
|
|
48
|
+
plugin.shutdown(on_disable)
|
|
49
|
+
|
|
50
|
+
return plugin
|
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
4
|
+
from dataclasses import asdict, dataclass, is_dataclass
|
|
5
|
+
import json
|
|
6
|
+
import inspect
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
_MISSING = object()
|
|
10
|
+
_SCALAR_TYPES = (str, int, float, bool, bytes)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _snake_to_camel(name: str) -> str:
|
|
14
|
+
if "_" not in name:
|
|
15
|
+
return name
|
|
16
|
+
|
|
17
|
+
head, *tail = name.split("_")
|
|
18
|
+
return head + "".join(part[:1].upper() + part[1:] for part in tail)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_member_name(raw: Any, name: str) -> str:
|
|
22
|
+
if hasattr(raw, name):
|
|
23
|
+
return name
|
|
24
|
+
|
|
25
|
+
camel_name = _snake_to_camel(name)
|
|
26
|
+
if hasattr(raw, camel_name):
|
|
27
|
+
return camel_name
|
|
28
|
+
|
|
29
|
+
raise AttributeError(f"{type(raw).__name__} has no attribute {name!r}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _accepts_argument(callback: Callable[..., Any]) -> bool:
|
|
33
|
+
try:
|
|
34
|
+
signature = inspect.signature(callback)
|
|
35
|
+
except (TypeError, ValueError):
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
for parameter in signature.parameters.values():
|
|
39
|
+
if parameter.kind in (
|
|
40
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
41
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
42
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
43
|
+
):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _wrap_guest_callable(callback: Callable[..., Any]) -> Callable[..., Any]:
|
|
50
|
+
def invoke(*args: Any, **kwargs: Any) -> Any:
|
|
51
|
+
if kwargs:
|
|
52
|
+
raise TypeError("Keyword arguments are not supported for raw runtime methods.")
|
|
53
|
+
return wrap(callback(*(_unwrap(arg) for arg in args)))
|
|
54
|
+
|
|
55
|
+
return invoke
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _wrap_callback(
|
|
59
|
+
callback: Callable[..., Any],
|
|
60
|
+
*,
|
|
61
|
+
payload_wrapper: Callable[[Any], Any] = lambda value: value,
|
|
62
|
+
) -> Callable[..., Any]:
|
|
63
|
+
accepts_argument = _accepts_argument(callback)
|
|
64
|
+
|
|
65
|
+
def invoke(payload: Any = _MISSING) -> Any:
|
|
66
|
+
if payload is _MISSING:
|
|
67
|
+
return callback()
|
|
68
|
+
if accepts_argument:
|
|
69
|
+
return callback(payload_wrapper(payload))
|
|
70
|
+
return callback()
|
|
71
|
+
|
|
72
|
+
return invoke
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _looks_like_location(value: Any) -> bool:
|
|
76
|
+
return all(hasattr(value, field) for field in ("x", "y", "z"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _looks_like_store(value: Any) -> bool:
|
|
80
|
+
return all(hasattr(value, field) for field in ("get", "set", "delete", "save", "reload", "keys"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _looks_like_file_store(value: Any) -> bool:
|
|
84
|
+
return all(hasattr(value, field) for field in ("path", "resolve", "readText", "writeText", "exists", "mkdirs"))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _camelize_mapping(mapping: Mapping[Any, Any]) -> dict[Any, Any]:
|
|
88
|
+
result: dict[Any, Any] = {}
|
|
89
|
+
for key, value in mapping.items():
|
|
90
|
+
normalized_key = _snake_to_camel(key) if isinstance(key, str) else key
|
|
91
|
+
if value is None:
|
|
92
|
+
continue
|
|
93
|
+
result[normalized_key] = _unwrap(value)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _unwrap(value: Any) -> Any:
|
|
98
|
+
if isinstance(value, GuestProxy):
|
|
99
|
+
return value.raw
|
|
100
|
+
|
|
101
|
+
if isinstance(value, Location):
|
|
102
|
+
return value.to_payload()
|
|
103
|
+
|
|
104
|
+
if value is None or isinstance(value, _SCALAR_TYPES):
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
if is_dataclass(value):
|
|
108
|
+
return _camelize_mapping(asdict(value))
|
|
109
|
+
|
|
110
|
+
if isinstance(value, Mapping):
|
|
111
|
+
return _camelize_mapping(value)
|
|
112
|
+
|
|
113
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
114
|
+
return [_unwrap(item) for item in value]
|
|
115
|
+
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def wrap(value: Any) -> Any:
|
|
120
|
+
if isinstance(value, (GuestProxy, Location)) or value is None or isinstance(value, _SCALAR_TYPES):
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
if isinstance(value, Mapping):
|
|
124
|
+
return MappingProxy(value)
|
|
125
|
+
|
|
126
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
127
|
+
return [wrap(item) for item in value]
|
|
128
|
+
|
|
129
|
+
if callable(value):
|
|
130
|
+
return _wrap_guest_callable(value)
|
|
131
|
+
|
|
132
|
+
if _looks_like_location(value):
|
|
133
|
+
return Location.from_raw(value)
|
|
134
|
+
|
|
135
|
+
if _looks_like_store(value):
|
|
136
|
+
return KeyValueStore(value)
|
|
137
|
+
|
|
138
|
+
if _looks_like_file_store(value):
|
|
139
|
+
return FileStore(value)
|
|
140
|
+
|
|
141
|
+
return GuestProxy(value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(slots=True)
|
|
145
|
+
class Location:
|
|
146
|
+
x: float
|
|
147
|
+
y: float
|
|
148
|
+
z: float
|
|
149
|
+
world: str | None = None
|
|
150
|
+
yaw: float | None = None
|
|
151
|
+
pitch: float | None = None
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_raw(cls, raw: Any) -> "Location":
|
|
155
|
+
return cls(
|
|
156
|
+
world=getattr(raw, "world", None),
|
|
157
|
+
x=getattr(raw, "x"),
|
|
158
|
+
y=getattr(raw, "y"),
|
|
159
|
+
z=getattr(raw, "z"),
|
|
160
|
+
yaw=getattr(raw, "yaw", None),
|
|
161
|
+
pitch=getattr(raw, "pitch", None),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def to_payload(self) -> dict[str, Any]:
|
|
165
|
+
return _camelize_mapping(asdict(self))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(slots=True)
|
|
169
|
+
class TitleOptions:
|
|
170
|
+
subtitle: Any | None = None
|
|
171
|
+
fade_in_ticks: int | None = None
|
|
172
|
+
stay_ticks: int | None = None
|
|
173
|
+
fade_out_ticks: int | None = None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class GuestProxy:
|
|
177
|
+
__slots__ = ("_raw",)
|
|
178
|
+
|
|
179
|
+
def __init__(self, raw: Any) -> None:
|
|
180
|
+
object.__setattr__(self, "_raw", raw)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def raw(self) -> Any:
|
|
184
|
+
return object.__getattribute__(self, "_raw")
|
|
185
|
+
|
|
186
|
+
def __getattr__(self, name: str) -> Any:
|
|
187
|
+
raw_name = _resolve_member_name(self.raw, name)
|
|
188
|
+
value = getattr(self.raw, raw_name)
|
|
189
|
+
|
|
190
|
+
if raw_name == "unsafe":
|
|
191
|
+
return UnsafeHandle(value)
|
|
192
|
+
|
|
193
|
+
if callable(value):
|
|
194
|
+
return _wrap_guest_callable(value)
|
|
195
|
+
|
|
196
|
+
return wrap(value)
|
|
197
|
+
|
|
198
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
199
|
+
if name.startswith("_"):
|
|
200
|
+
object.__setattr__(self, name, value)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
raw_name = _resolve_member_name(self.raw, name)
|
|
204
|
+
setattr(self.raw, raw_name, _unwrap(value))
|
|
205
|
+
|
|
206
|
+
def __repr__(self) -> str:
|
|
207
|
+
return f"{self.__class__.__name__}({self.raw!r})"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class MappingProxy(GuestProxy):
|
|
211
|
+
def __getattr__(self, name: str) -> Any:
|
|
212
|
+
if name in self.raw:
|
|
213
|
+
return wrap(self.raw[name])
|
|
214
|
+
|
|
215
|
+
camel_name = _snake_to_camel(name)
|
|
216
|
+
if camel_name in self.raw:
|
|
217
|
+
return wrap(self.raw[camel_name])
|
|
218
|
+
|
|
219
|
+
raise AttributeError(f"mapping has no attribute {name!r}")
|
|
220
|
+
|
|
221
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
222
|
+
if name.startswith("_"):
|
|
223
|
+
object.__setattr__(self, name, value)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
if name in self.raw:
|
|
227
|
+
self.raw[name] = _unwrap(value)
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
self.raw[_snake_to_camel(name)] = _unwrap(value)
|
|
231
|
+
|
|
232
|
+
def __getitem__(self, key: Any) -> Any:
|
|
233
|
+
return wrap(self.raw[key])
|
|
234
|
+
|
|
235
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
|
236
|
+
if key in self.raw:
|
|
237
|
+
return wrap(self.raw[key])
|
|
238
|
+
return default
|
|
239
|
+
|
|
240
|
+
def items(self) -> list[tuple[Any, Any]]:
|
|
241
|
+
return [(key, wrap(value)) for key, value in self.raw.items()]
|
|
242
|
+
|
|
243
|
+
def keys(self) -> list[Any]:
|
|
244
|
+
return list(self.raw.keys())
|
|
245
|
+
|
|
246
|
+
def values(self) -> list[Any]:
|
|
247
|
+
return [wrap(value) for value in self.raw.values()]
|
|
248
|
+
|
|
249
|
+
def __contains__(self, key: Any) -> bool:
|
|
250
|
+
return key in self.raw
|
|
251
|
+
|
|
252
|
+
def __iter__(self):
|
|
253
|
+
return iter(self.raw)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class UnsafeHandle(GuestProxy):
|
|
257
|
+
@property
|
|
258
|
+
def handle(self) -> Any:
|
|
259
|
+
return getattr(self.raw, "handle")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class Logger(GuestProxy):
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class HookHandle(GuestProxy):
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TaskHandle(GuestProxy):
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class KeyValueStore(GuestProxy):
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class FileStore(GuestProxy):
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class AppService(GuestProxy):
|
|
283
|
+
@property
|
|
284
|
+
def log(self) -> Logger:
|
|
285
|
+
return Logger(getattr(self.raw, "log"))
|
|
286
|
+
|
|
287
|
+
def on_shutdown(self, handler: Callable[..., Any]) -> HookHandle:
|
|
288
|
+
return HookHandle(getattr(self.raw, "onShutdown")(_wrap_callback(handler)))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class CommandRegistry(GuestProxy):
|
|
292
|
+
def register(
|
|
293
|
+
self,
|
|
294
|
+
name: str | Mapping[str, Any] | None = None,
|
|
295
|
+
execute: Callable[..., Any] | None = None,
|
|
296
|
+
**kwargs: Any,
|
|
297
|
+
) -> HookHandle:
|
|
298
|
+
if isinstance(name, Mapping):
|
|
299
|
+
if execute is not None or kwargs:
|
|
300
|
+
raise TypeError("register() accepts either a mapping or keyword arguments, not both.")
|
|
301
|
+
spec = dict(name)
|
|
302
|
+
else:
|
|
303
|
+
spec = dict(kwargs)
|
|
304
|
+
if name is not None:
|
|
305
|
+
spec["name"] = name
|
|
306
|
+
if execute is not None:
|
|
307
|
+
spec["execute"] = execute
|
|
308
|
+
|
|
309
|
+
callback = spec.get("execute")
|
|
310
|
+
if callback is None:
|
|
311
|
+
raise TypeError("register() requires an execute callback.")
|
|
312
|
+
|
|
313
|
+
spec["execute"] = _wrap_callback(callback, payload_wrapper=wrap)
|
|
314
|
+
return HookHandle(getattr(self.raw, "register")(_unwrap(spec)))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class EventRegistry(GuestProxy):
|
|
318
|
+
def on(self, event_name: str, handler: Callable[..., Any]) -> HookHandle:
|
|
319
|
+
return HookHandle(getattr(self.raw, "on")(event_name, _wrap_callback(handler, payload_wrapper=wrap)))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class TaskRegistry(GuestProxy):
|
|
323
|
+
def run(self, task: Callable[..., Any]) -> TaskHandle:
|
|
324
|
+
return TaskHandle(getattr(self.raw, "run")(_wrap_callback(task)))
|
|
325
|
+
|
|
326
|
+
def delay(self, delay_ticks: int, task: Callable[..., Any]) -> TaskHandle:
|
|
327
|
+
return TaskHandle(getattr(self.raw, "delay")(delay_ticks, _wrap_callback(task)))
|
|
328
|
+
|
|
329
|
+
def repeat(self, interval_ticks: int, task: Callable[..., Any]) -> TaskHandle:
|
|
330
|
+
return TaskHandle(getattr(self.raw, "repeat")(interval_ticks, _wrap_callback(task)))
|
|
331
|
+
|
|
332
|
+
def timer(self, delay_ticks: int, interval_ticks: int, task: Callable[..., Any]) -> TaskHandle:
|
|
333
|
+
return TaskHandle(getattr(self.raw, "timer")(delay_ticks, interval_ticks, _wrap_callback(task)))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class PlayerDirectory(GuestProxy):
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class WorldDirectory(GuestProxy):
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class EntityDirectory(GuestProxy):
|
|
345
|
+
def spawn(
|
|
346
|
+
self,
|
|
347
|
+
entity_type: str | Mapping[str, Any],
|
|
348
|
+
location: Any | None = None,
|
|
349
|
+
*,
|
|
350
|
+
world: str | None = None,
|
|
351
|
+
) -> GuestProxy:
|
|
352
|
+
if isinstance(entity_type, Mapping):
|
|
353
|
+
if location is not None or world is not None:
|
|
354
|
+
raise TypeError("spawn() accepts either a mapping or explicit arguments, not both.")
|
|
355
|
+
spec = dict(entity_type)
|
|
356
|
+
else:
|
|
357
|
+
if location is None:
|
|
358
|
+
raise TypeError("spawn() requires a location.")
|
|
359
|
+
spec = {"type": entity_type, "location": location, "world": world}
|
|
360
|
+
|
|
361
|
+
return wrap(getattr(self.raw, "spawn")(_unwrap(spec)))
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class ItemFactory(GuestProxy):
|
|
365
|
+
def create(self, item_type: str | Mapping[str, Any], **kwargs: Any) -> GuestProxy:
|
|
366
|
+
if isinstance(item_type, Mapping):
|
|
367
|
+
if kwargs:
|
|
368
|
+
raise TypeError("create() accepts either a mapping or keyword arguments, not both.")
|
|
369
|
+
spec = dict(item_type)
|
|
370
|
+
else:
|
|
371
|
+
spec = {"type": item_type, **kwargs}
|
|
372
|
+
|
|
373
|
+
return wrap(getattr(self.raw, "create")(_unwrap(spec)))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class InventoryDirectory(GuestProxy):
|
|
377
|
+
def create(self, title: Any | Mapping[str, Any], size: int | None = None, *, id: str | None = None) -> GuestProxy:
|
|
378
|
+
if isinstance(title, Mapping):
|
|
379
|
+
if size is not None or id is not None:
|
|
380
|
+
raise TypeError("create() accepts either a mapping or explicit arguments, not both.")
|
|
381
|
+
spec = dict(title)
|
|
382
|
+
else:
|
|
383
|
+
if size is None:
|
|
384
|
+
raise TypeError("create() requires a size.")
|
|
385
|
+
spec = {"title": title, "size": size, "id": id}
|
|
386
|
+
|
|
387
|
+
return wrap(getattr(self.raw, "create")(_unwrap(spec)))
|
|
388
|
+
|
|
389
|
+
def open(self, player: Any, inventory: Any) -> None:
|
|
390
|
+
getattr(self.raw, "open")(_unwrap(player), _unwrap(inventory))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class ChatService(GuestProxy):
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class EffectsService(GuestProxy):
|
|
398
|
+
def play_sound(
|
|
399
|
+
self,
|
|
400
|
+
sound: str | Mapping[str, Any],
|
|
401
|
+
*,
|
|
402
|
+
location: Any | None = None,
|
|
403
|
+
player: Any | None = None,
|
|
404
|
+
volume: float | None = None,
|
|
405
|
+
pitch: float | None = None,
|
|
406
|
+
) -> None:
|
|
407
|
+
if isinstance(sound, Mapping):
|
|
408
|
+
spec = dict(sound)
|
|
409
|
+
else:
|
|
410
|
+
spec = {
|
|
411
|
+
"sound": sound,
|
|
412
|
+
"location": location,
|
|
413
|
+
"player": player,
|
|
414
|
+
"volume": volume,
|
|
415
|
+
"pitch": pitch,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
getattr(self.raw, "playSound")(_unwrap(spec))
|
|
419
|
+
|
|
420
|
+
def spawn_particle(
|
|
421
|
+
self,
|
|
422
|
+
particle: str | Mapping[str, Any],
|
|
423
|
+
*,
|
|
424
|
+
location: Any | None = None,
|
|
425
|
+
count: int | None = None,
|
|
426
|
+
offset_x: float | None = None,
|
|
427
|
+
offset_y: float | None = None,
|
|
428
|
+
offset_z: float | None = None,
|
|
429
|
+
extra: float | None = None,
|
|
430
|
+
players: Sequence[Any] | None = None,
|
|
431
|
+
) -> None:
|
|
432
|
+
if isinstance(particle, Mapping):
|
|
433
|
+
spec = dict(particle)
|
|
434
|
+
else:
|
|
435
|
+
spec = {
|
|
436
|
+
"particle": particle,
|
|
437
|
+
"location": location,
|
|
438
|
+
"count": count,
|
|
439
|
+
"offset_x": offset_x,
|
|
440
|
+
"offset_y": offset_y,
|
|
441
|
+
"offset_z": offset_z,
|
|
442
|
+
"extra": extra,
|
|
443
|
+
"players": players,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
getattr(self.raw, "spawnParticle")(_unwrap(spec))
|
|
447
|
+
|
|
448
|
+
def apply_potion(
|
|
449
|
+
self,
|
|
450
|
+
player: Any | Mapping[str, Any],
|
|
451
|
+
*,
|
|
452
|
+
effect: str | None = None,
|
|
453
|
+
duration_ticks: int | None = None,
|
|
454
|
+
amplifier: int | None = None,
|
|
455
|
+
ambient: bool | None = None,
|
|
456
|
+
particles: bool | None = None,
|
|
457
|
+
icon: bool | None = None,
|
|
458
|
+
) -> bool:
|
|
459
|
+
if isinstance(player, Mapping):
|
|
460
|
+
spec = dict(player)
|
|
461
|
+
else:
|
|
462
|
+
spec = {
|
|
463
|
+
"player": player,
|
|
464
|
+
"effect": effect,
|
|
465
|
+
"duration_ticks": duration_ticks,
|
|
466
|
+
"amplifier": amplifier,
|
|
467
|
+
"ambient": ambient,
|
|
468
|
+
"particles": particles,
|
|
469
|
+
"icon": icon,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return bool(getattr(self.raw, "applyPotion")(_unwrap(spec)))
|
|
473
|
+
|
|
474
|
+
def clear_potion(self, player: Any, effect: str | None = None) -> None:
|
|
475
|
+
if effect is None:
|
|
476
|
+
getattr(self.raw, "clearPotion")(_unwrap(player))
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
getattr(self.raw, "clearPotion")(_unwrap(player), effect)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class RecipeService(GuestProxy):
|
|
483
|
+
def register(self, spec: Mapping[str, Any]) -> GuestProxy:
|
|
484
|
+
return wrap(getattr(self.raw, "register")(_unwrap(spec)))
|
|
485
|
+
|
|
486
|
+
def register_shaped(
|
|
487
|
+
self,
|
|
488
|
+
*,
|
|
489
|
+
id: str,
|
|
490
|
+
result: Mapping[str, Any],
|
|
491
|
+
shape: Sequence[str],
|
|
492
|
+
ingredients: Mapping[str, Any],
|
|
493
|
+
) -> GuestProxy:
|
|
494
|
+
return self.register(
|
|
495
|
+
{
|
|
496
|
+
"kind": "shaped",
|
|
497
|
+
"id": id,
|
|
498
|
+
"result": result,
|
|
499
|
+
"shape": list(shape),
|
|
500
|
+
"ingredients": dict(ingredients),
|
|
501
|
+
},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def register_shapeless(
|
|
505
|
+
self,
|
|
506
|
+
*,
|
|
507
|
+
id: str,
|
|
508
|
+
result: Mapping[str, Any],
|
|
509
|
+
ingredients: Sequence[Any],
|
|
510
|
+
) -> GuestProxy:
|
|
511
|
+
return self.register(
|
|
512
|
+
{
|
|
513
|
+
"kind": "shapeless",
|
|
514
|
+
"id": id,
|
|
515
|
+
"result": result,
|
|
516
|
+
"ingredients": list(ingredients),
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class BossBarsService(GuestProxy):
|
|
522
|
+
def create(
|
|
523
|
+
self,
|
|
524
|
+
title: Any | Mapping[str, Any],
|
|
525
|
+
*,
|
|
526
|
+
id: str | None = None,
|
|
527
|
+
color: str | None = None,
|
|
528
|
+
style: str | None = None,
|
|
529
|
+
progress: float | None = None,
|
|
530
|
+
) -> GuestProxy:
|
|
531
|
+
if isinstance(title, Mapping):
|
|
532
|
+
spec = dict(title)
|
|
533
|
+
else:
|
|
534
|
+
spec = {
|
|
535
|
+
"title": title,
|
|
536
|
+
"id": id,
|
|
537
|
+
"color": color,
|
|
538
|
+
"style": style,
|
|
539
|
+
"progress": progress,
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return wrap(getattr(self.raw, "create")(_unwrap(spec)))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class ScoreboardsService(GuestProxy):
|
|
546
|
+
def create(self, title: Any | Mapping[str, Any], *, id: str | None = None) -> GuestProxy:
|
|
547
|
+
if isinstance(title, Mapping):
|
|
548
|
+
spec = dict(title)
|
|
549
|
+
else:
|
|
550
|
+
spec = {"title": title, "id": id}
|
|
551
|
+
|
|
552
|
+
return wrap(getattr(self.raw, "create")(_unwrap(spec)))
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class StorageService:
|
|
556
|
+
__slots__ = ("plugin", "files", "raw")
|
|
557
|
+
|
|
558
|
+
def __init__(self, raw: Any) -> None:
|
|
559
|
+
self.raw = raw
|
|
560
|
+
self.plugin = KeyValueStore(getattr(raw, "plugin"))
|
|
561
|
+
self.files = FileStore(getattr(raw, "files"))
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@dataclass(slots=True)
|
|
565
|
+
class HttpResponse:
|
|
566
|
+
url: str
|
|
567
|
+
status: int
|
|
568
|
+
ok: bool
|
|
569
|
+
headers: dict[str, str]
|
|
570
|
+
body: str
|
|
571
|
+
|
|
572
|
+
@classmethod
|
|
573
|
+
def from_raw(cls, raw: Any) -> "HttpResponse":
|
|
574
|
+
raw_headers = wrap(getattr(raw, "headers"))
|
|
575
|
+
return cls(
|
|
576
|
+
url=getattr(raw, "url"),
|
|
577
|
+
status=getattr(raw, "status"),
|
|
578
|
+
ok=bool(getattr(raw, "ok")),
|
|
579
|
+
headers={key: raw_headers[key] for key in raw_headers},
|
|
580
|
+
body=getattr(raw, "body"),
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def text(self) -> str:
|
|
585
|
+
return self.body
|
|
586
|
+
|
|
587
|
+
def json(self) -> Any:
|
|
588
|
+
return json.loads(self.body)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class HttpService(GuestProxy):
|
|
592
|
+
def fetch(
|
|
593
|
+
self,
|
|
594
|
+
url: str | Mapping[str, Any],
|
|
595
|
+
method: str | None = None,
|
|
596
|
+
*,
|
|
597
|
+
headers: Mapping[str, str] | None = None,
|
|
598
|
+
body: str | None = None,
|
|
599
|
+
) -> HttpResponse:
|
|
600
|
+
if isinstance(url, Mapping):
|
|
601
|
+
if method is not None or headers is not None or body is not None:
|
|
602
|
+
raise TypeError("fetch() accepts either a mapping or explicit arguments, not both.")
|
|
603
|
+
spec = dict(url)
|
|
604
|
+
else:
|
|
605
|
+
spec = {
|
|
606
|
+
"url": url,
|
|
607
|
+
"method": method,
|
|
608
|
+
"headers": headers,
|
|
609
|
+
"body": body,
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return HttpResponse.from_raw(getattr(self.raw, "fetch")(_unwrap(spec)))
|
|
613
|
+
|
|
614
|
+
def get(self, url: str, *, headers: Mapping[str, str] | None = None) -> HttpResponse:
|
|
615
|
+
return self.fetch(url, headers=headers)
|
|
616
|
+
|
|
617
|
+
def post(
|
|
618
|
+
self,
|
|
619
|
+
url: str,
|
|
620
|
+
*,
|
|
621
|
+
headers: Mapping[str, str] | None = None,
|
|
622
|
+
body: str | None = None,
|
|
623
|
+
) -> HttpResponse:
|
|
624
|
+
return self.fetch(url, method="POST", headers=headers, body=body)
|
|
625
|
+
|
|
626
|
+
def put(
|
|
627
|
+
self,
|
|
628
|
+
url: str,
|
|
629
|
+
*,
|
|
630
|
+
headers: Mapping[str, str] | None = None,
|
|
631
|
+
body: str | None = None,
|
|
632
|
+
) -> HttpResponse:
|
|
633
|
+
return self.fetch(url, method="PUT", headers=headers, body=body)
|
|
634
|
+
|
|
635
|
+
def delete(self, url: str, *, headers: Mapping[str, str] | None = None) -> HttpResponse:
|
|
636
|
+
return self.fetch(url, method="DELETE", headers=headers)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class UnsafeEvents(GuestProxy):
|
|
640
|
+
def on_java(self, event_class_name: str, handler: Callable[..., Any]) -> HookHandle:
|
|
641
|
+
return HookHandle(getattr(self.raw, "onJava")(event_class_name, _wrap_callback(handler, payload_wrapper=wrap)))
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
class UnsafeJava(GuestProxy):
|
|
645
|
+
def type(self, class_name: str) -> Any:
|
|
646
|
+
return getattr(self.raw, "type")(class_name)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
class UnsafeBackend(GuestProxy):
|
|
650
|
+
@property
|
|
651
|
+
def server(self) -> Any:
|
|
652
|
+
return getattr(self.raw, "server")
|
|
653
|
+
|
|
654
|
+
@property
|
|
655
|
+
def plugin(self) -> Any:
|
|
656
|
+
return getattr(self.raw, "plugin")
|
|
657
|
+
|
|
658
|
+
@property
|
|
659
|
+
def console(self) -> Any:
|
|
660
|
+
return getattr(self.raw, "console")
|
|
661
|
+
|
|
662
|
+
def dispatch_command(self, command: str) -> bool:
|
|
663
|
+
return bool(getattr(self.raw, "dispatchCommand")(command))
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
class UnsafeBridge:
|
|
667
|
+
__slots__ = ("events", "java", "backend", "raw")
|
|
668
|
+
|
|
669
|
+
def __init__(self, raw: Any) -> None:
|
|
670
|
+
self.raw = raw
|
|
671
|
+
self.events = UnsafeEvents(getattr(raw, "events"))
|
|
672
|
+
self.java = UnsafeJava(getattr(raw, "java"))
|
|
673
|
+
self.backend = UnsafeBackend(getattr(raw, "backend"))
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class PluginContext:
|
|
677
|
+
__slots__ = (
|
|
678
|
+
"app",
|
|
679
|
+
"boss_bars",
|
|
680
|
+
"chat",
|
|
681
|
+
"commands",
|
|
682
|
+
"config",
|
|
683
|
+
"effects",
|
|
684
|
+
"entities",
|
|
685
|
+
"events",
|
|
686
|
+
"http",
|
|
687
|
+
"inventory",
|
|
688
|
+
"items",
|
|
689
|
+
"players",
|
|
690
|
+
"raw",
|
|
691
|
+
"recipes",
|
|
692
|
+
"scoreboards",
|
|
693
|
+
"storage",
|
|
694
|
+
"tasks",
|
|
695
|
+
"unsafe",
|
|
696
|
+
"worlds",
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
def __init__(self, raw: Any) -> None:
|
|
700
|
+
self.raw = raw
|
|
701
|
+
self.app = AppService(getattr(raw, "app"))
|
|
702
|
+
self.commands = CommandRegistry(getattr(raw, "commands"))
|
|
703
|
+
self.events = EventRegistry(getattr(raw, "events"))
|
|
704
|
+
self.tasks = TaskRegistry(getattr(raw, "tasks"))
|
|
705
|
+
self.players = PlayerDirectory(getattr(raw, "players"))
|
|
706
|
+
self.worlds = WorldDirectory(getattr(raw, "worlds"))
|
|
707
|
+
self.entities = EntityDirectory(getattr(raw, "entities"))
|
|
708
|
+
self.items = ItemFactory(getattr(raw, "items"))
|
|
709
|
+
self.inventory = InventoryDirectory(getattr(raw, "inventory"))
|
|
710
|
+
self.chat = ChatService(getattr(raw, "chat"))
|
|
711
|
+
self.effects = EffectsService(getattr(raw, "effects"))
|
|
712
|
+
self.recipes = RecipeService(getattr(raw, "recipes"))
|
|
713
|
+
self.boss_bars = BossBarsService(getattr(raw, "bossBars"))
|
|
714
|
+
self.scoreboards = ScoreboardsService(getattr(raw, "scoreboards"))
|
|
715
|
+
self.storage = StorageService(getattr(raw, "storage"))
|
|
716
|
+
self.http = HttpService(getattr(raw, "http"))
|
|
717
|
+
self.config = KeyValueStore(getattr(raw, "config"))
|
|
718
|
+
self.unsafe = UnsafeBridge(getattr(raw, "unsafe"))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from lapis_lazuli import Plugin, PluginContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RawLogger:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.messages: list[str] = []
|
|
11
|
+
|
|
12
|
+
def info(self, message: str) -> None:
|
|
13
|
+
self.messages.append(message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RawApp:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self.id = "hello-python"
|
|
19
|
+
self.name = "Hello Python"
|
|
20
|
+
self.version = "0.1.0"
|
|
21
|
+
self.engine = "python"
|
|
22
|
+
self.apiVersion = "1.0"
|
|
23
|
+
self.backend = "paper"
|
|
24
|
+
self.runtime = "test"
|
|
25
|
+
self.log = RawLogger()
|
|
26
|
+
|
|
27
|
+
def onShutdown(self, handler):
|
|
28
|
+
self.handler = handler
|
|
29
|
+
return RawHandle()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RawHandle:
|
|
33
|
+
def unsubscribe(self) -> None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def cancel(self) -> None:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RawSender:
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self.messages: list[str] = []
|
|
43
|
+
|
|
44
|
+
def sendMessage(self, message: str) -> None:
|
|
45
|
+
self.messages.append(message)
|
|
46
|
+
|
|
47
|
+
def hasPermission(self, permission: str) -> bool:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RawCommands:
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self.registrations: list[dict[str, object]] = []
|
|
54
|
+
|
|
55
|
+
def register(self, spec):
|
|
56
|
+
self.registrations.append(spec)
|
|
57
|
+
return RawHandle()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RawEvents:
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self.handlers: dict[str, object] = {}
|
|
63
|
+
|
|
64
|
+
def on(self, event_name, handler):
|
|
65
|
+
self.handlers[event_name] = handler
|
|
66
|
+
return RawHandle()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RawNoop:
|
|
70
|
+
def run(self, handler):
|
|
71
|
+
self.handler = handler
|
|
72
|
+
return RawHandle()
|
|
73
|
+
|
|
74
|
+
def delay(self, delay_ticks, handler):
|
|
75
|
+
self.delay_ticks = delay_ticks
|
|
76
|
+
self.delay_handler = handler
|
|
77
|
+
return RawHandle()
|
|
78
|
+
|
|
79
|
+
def repeat(self, interval_ticks, handler):
|
|
80
|
+
self.interval_ticks = interval_ticks
|
|
81
|
+
self.repeat_handler = handler
|
|
82
|
+
return RawHandle()
|
|
83
|
+
|
|
84
|
+
def timer(self, delay_ticks, interval_ticks, handler):
|
|
85
|
+
self.delay_ticks = delay_ticks
|
|
86
|
+
self.interval_ticks = interval_ticks
|
|
87
|
+
self.timer_handler = handler
|
|
88
|
+
return RawHandle()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RawStore:
|
|
92
|
+
def get(self, path):
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def set(self, path, value):
|
|
96
|
+
self.last_set = (path, value)
|
|
97
|
+
|
|
98
|
+
def delete(self, path):
|
|
99
|
+
self.deleted = path
|
|
100
|
+
|
|
101
|
+
def save(self):
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def reload(self):
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def keys(self):
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RawFiles:
|
|
112
|
+
path = "/tmp"
|
|
113
|
+
|
|
114
|
+
def resolve(self, *segments):
|
|
115
|
+
return "/tmp/" + "/".join(segments)
|
|
116
|
+
|
|
117
|
+
def readText(self, relative_path):
|
|
118
|
+
return relative_path
|
|
119
|
+
|
|
120
|
+
def writeText(self, relative_path, contents):
|
|
121
|
+
self.last_write = (relative_path, contents)
|
|
122
|
+
|
|
123
|
+
def exists(self, relative_path):
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
def mkdirs(self, relative_path=""):
|
|
127
|
+
self.last_mkdir = relative_path
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class RawStorage:
|
|
131
|
+
def __init__(self) -> None:
|
|
132
|
+
self.plugin = RawStore()
|
|
133
|
+
self.files = RawFiles()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class RawHttpResponse:
|
|
137
|
+
def __init__(self, url: str, status: int, headers: dict[str, str], body: str) -> None:
|
|
138
|
+
self.url = url
|
|
139
|
+
self.status = status
|
|
140
|
+
self.ok = 200 <= status <= 299
|
|
141
|
+
self.headers = headers
|
|
142
|
+
self.body = body
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RawHttp:
|
|
146
|
+
def fetch(self, spec):
|
|
147
|
+
self.last_request = spec
|
|
148
|
+
return RawHttpResponse(
|
|
149
|
+
url=spec["url"],
|
|
150
|
+
status=201,
|
|
151
|
+
headers={"content-type": "application/json"},
|
|
152
|
+
body='{"ok": true}',
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class RawUnsafeEvents:
|
|
157
|
+
def onJava(self, event_class_name, handler):
|
|
158
|
+
self.last_handler = (event_class_name, handler)
|
|
159
|
+
return RawHandle()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class RawUnsafeJava:
|
|
163
|
+
def type(self, class_name):
|
|
164
|
+
return class_name
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class RawUnsafeBackend:
|
|
168
|
+
server = object()
|
|
169
|
+
plugin = object()
|
|
170
|
+
console = object()
|
|
171
|
+
|
|
172
|
+
def dispatchCommand(self, command):
|
|
173
|
+
self.last_command = command
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class RawUnsafe:
|
|
178
|
+
def __init__(self) -> None:
|
|
179
|
+
self.events = RawUnsafeEvents()
|
|
180
|
+
self.java = RawUnsafeJava()
|
|
181
|
+
self.backend = RawUnsafeBackend()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class RawContext:
|
|
185
|
+
def __init__(self) -> None:
|
|
186
|
+
self.app = RawApp()
|
|
187
|
+
self.commands = RawCommands()
|
|
188
|
+
self.events = RawEvents()
|
|
189
|
+
self.tasks = RawNoop()
|
|
190
|
+
self.players = RawNoop()
|
|
191
|
+
self.worlds = RawNoop()
|
|
192
|
+
self.entities = RawNoop()
|
|
193
|
+
self.items = RawNoop()
|
|
194
|
+
self.inventory = RawNoop()
|
|
195
|
+
self.chat = RawNoop()
|
|
196
|
+
self.effects = RawNoop()
|
|
197
|
+
self.recipes = RawNoop()
|
|
198
|
+
self.bossBars = RawNoop()
|
|
199
|
+
self.scoreboards = RawNoop()
|
|
200
|
+
self.storage = RawStorage()
|
|
201
|
+
self.http = RawHttp()
|
|
202
|
+
self.config = RawStore()
|
|
203
|
+
self.unsafe = RawUnsafe()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class RawCommandContext:
|
|
207
|
+
def __init__(self, sender: RawSender) -> None:
|
|
208
|
+
self.sender = sender
|
|
209
|
+
self.args = []
|
|
210
|
+
self.label = "hello"
|
|
211
|
+
self.command = "hello"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PluginTests(unittest.TestCase):
|
|
215
|
+
def test_plugin_wraps_context_and_registers_pythonic_command(self) -> None:
|
|
216
|
+
raw_context = RawContext()
|
|
217
|
+
plugin = Plugin("Hello Python", version="0.1.0")
|
|
218
|
+
|
|
219
|
+
@plugin.startup
|
|
220
|
+
def on_enable(context: PluginContext) -> None:
|
|
221
|
+
context.app.log.info("enabled")
|
|
222
|
+
context.commands.register(
|
|
223
|
+
"hello",
|
|
224
|
+
lambda command: command.sender.send_message("Hello from Python."),
|
|
225
|
+
description="Send a greeting.",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
plugin.on_enable(raw_context)
|
|
229
|
+
|
|
230
|
+
self.assertEqual(["enabled"], raw_context.app.log.messages)
|
|
231
|
+
registration = raw_context.commands.registrations[0]
|
|
232
|
+
self.assertEqual("hello", registration["name"])
|
|
233
|
+
self.assertEqual("Send a greeting.", registration["description"])
|
|
234
|
+
|
|
235
|
+
sender = RawSender()
|
|
236
|
+
registration["execute"](RawCommandContext(sender))
|
|
237
|
+
self.assertEqual(["Hello from Python."], sender.messages)
|
|
238
|
+
|
|
239
|
+
def test_http_service_wraps_requests_pythonically(self) -> None:
|
|
240
|
+
raw_context = RawContext()
|
|
241
|
+
context = PluginContext(raw_context)
|
|
242
|
+
|
|
243
|
+
response = context.http.post(
|
|
244
|
+
"https://example.test/items",
|
|
245
|
+
headers={"content-type": "application/json"},
|
|
246
|
+
body='{"name":"lapis"}',
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.assertEqual(
|
|
250
|
+
{
|
|
251
|
+
"url": "https://example.test/items",
|
|
252
|
+
"method": "POST",
|
|
253
|
+
"headers": {"content-type": "application/json"},
|
|
254
|
+
"body": '{"name":"lapis"}',
|
|
255
|
+
},
|
|
256
|
+
raw_context.http.last_request,
|
|
257
|
+
)
|
|
258
|
+
self.assertEqual(201, response.status)
|
|
259
|
+
self.assertTrue(response.ok)
|
|
260
|
+
self.assertEqual("application/json", response.headers["content-type"])
|
|
261
|
+
self.assertEqual({"ok": True}, response.json())
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
unittest.main()
|