ZedScripts 0.0.2__py3-none-any.whl

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.
@@ -0,0 +1,355 @@
1
+ import logging
2
+ import re
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, TypedDict
5
+ from pprint import pformat
6
+
7
+ from .. import SCRIPTS_BLOCKS_DATA_LINK, ROOTS_DATA_LINK
8
+ from . import ScriptBlockData, ScriptBlockParameter, ValueType, cant_self_validate
9
+ from .version import GameVersion, DataVersion
10
+ from .. import SCRIPTS_DATA_MANIFEST
11
+ from ..providers.http import load_json
12
+ from ..environment.config import DatasetModel, DatasetTag
13
+
14
+ if TYPE_CHECKING:
15
+ from ..environment.config import ReleaseModel
16
+
17
+
18
+ class Release(TypedDict):
19
+ date: str
20
+ version: int
21
+
22
+ class Datasets(TypedDict):
23
+ blocks: str
24
+ roots: str
25
+
26
+
27
+
28
+ class Manifest:
29
+ instance: 'Manifest | None' = None
30
+
31
+ def __init__(self, latest_build: GameVersion, releases: dict[GameVersion, Release], stable: DataVersion):
32
+ self.latest_build: GameVersion = latest_build
33
+ self.releases: dict[GameVersion, Release] = releases
34
+ self.stable: DataVersion = stable
35
+
36
+ self.releases_list: list[str] = self.make_releases_list()
37
+
38
+ # cache manifest
39
+ Manifest.instance = self
40
+
41
+ def __repr__(self) -> str:
42
+ return f"Manifest(latest_build={self.latest_build}, releases={len(self.releases)}, stable={self.stable})"
43
+
44
+ def make_releases_list(self) -> list[str]:
45
+ """Transforms into a list of strings all available dataset versions."""
46
+ releases_list: list[str] = []
47
+ for version, release in self.releases.items():
48
+ for v in range(release['version']):
49
+ releases_list.append(str(version.to_data_version(v)))
50
+ return sorted(releases_list)
51
+
52
+ @staticmethod
53
+ def from_dict(data: dict) -> "Manifest":
54
+ return Manifest(
55
+ latest_build=GameVersion.find_or_make(data["latest_build"]),
56
+ releases={GameVersion.find_or_make(k): v for k, v in data["releases"].items()},
57
+ stable=DataVersion(data["stable"]),
58
+ )
59
+
60
+ @staticmethod
61
+ def find_release(game_version: GameVersion) -> Release | None:
62
+ assert Manifest.instance is not None
63
+ for version, release in Manifest.instance.releases.items():
64
+ if version == game_version:
65
+ return release
66
+ return None
67
+
68
+ @staticmethod
69
+ def get_latest_version(major: int, minor: int, patch: int) -> int:
70
+ assert Manifest.instance is not None
71
+ release = Manifest.find_release(GameVersion.from_values(major, minor, patch))
72
+ if release is None:
73
+ raise ValueError(f"No release found for version {major}.{minor}.{patch}")
74
+ return release['version']
75
+
76
+ @staticmethod
77
+ def from_release_model(dataset_config: 'ReleaseModel') -> 'DataVersion':
78
+ game_version = GameVersion.from_values(dataset_config.major, dataset_config.minor, dataset_config.patch)
79
+ version = dataset_config.version
80
+ if version is None:
81
+ version = Manifest.get_latest_version(dataset_config.major, dataset_config.minor, dataset_config.patch)
82
+ data_version = game_version.to_data_version(version)
83
+ return data_version
84
+
85
+
86
+
87
+ @staticmethod
88
+ def load() -> "Manifest":
89
+ # if for some reason the LSP gets active a long time
90
+ # this will not get updated (but tbh this should absolutely not be a problem)
91
+ if Manifest.instance is not None:
92
+ return Manifest.instance
93
+
94
+ # load the manifest from the JSON file
95
+ logging.info("Loading manifest...")
96
+ manifest_data = load_json(SCRIPTS_DATA_MANIFEST)
97
+ if manifest_data is None:
98
+ raise RuntimeError("Failed to load manifest data")
99
+
100
+ logging.debug(pformat(manifest_data))
101
+ return Manifest.from_dict(manifest_data)
102
+
103
+ def get_latest(self) -> tuple[DataVersion,Release]:
104
+ versions = sorted(self.releases.keys())
105
+ latest_version = versions[-1]
106
+ release = self.releases[latest_version]
107
+ return latest_version.to_data_version(release['version']), release
108
+
109
+ def get_stable(self) -> DataVersion:
110
+ stable_version = self.stable
111
+ return stable_version
112
+
113
+
114
+ def get_best_dataset_tag(self, key: DataVersion | DatasetTag) -> DataVersion:
115
+ # find classic keyword cases
116
+ match key:
117
+ case DatasetTag.LATEST:
118
+ return self.get_latest()[0]
119
+ case DatasetTag.STABLE:
120
+ return self.get_stable()
121
+ case _:
122
+ if not isinstance(key, DataVersion):
123
+ return NotImplemented
124
+
125
+ # try to find the key in available releases
126
+ to_find = str(key)
127
+ releases_list = self.releases_list
128
+
129
+ pos: int | None = releases_list.index(to_find) if to_find in releases_list else None
130
+ if pos is not None:
131
+ return DataVersion(releases_list[pos])
132
+
133
+ # if not found, return stable
134
+ logging.warning("Dataset version not found, falling back to stable.")
135
+ return self.get_stable()
136
+
137
+
138
+
139
+ def get_dataset_links(self, dataset_config: DatasetModel) -> Datasets:
140
+ """
141
+ Retrieve the dataset links for the specified tag_key.
142
+ """
143
+ key = dataset_config.to_data_version()
144
+ best_tag = self.get_best_dataset_tag(key)
145
+ if best_tag is None:
146
+ raise ValueError(f"No dataset found for key: {key}")
147
+
148
+ return {
149
+ "blocks": SCRIPTS_BLOCKS_DATA_LINK.format(best_tag=best_tag),
150
+ "roots": ROOTS_DATA_LINK.format(best_tag=best_tag),
151
+ }
152
+
153
+
154
+
155
+
156
+
157
+
158
+ class Dataset:
159
+ def __init__(self):
160
+ self.manifest: Manifest = Manifest.load()
161
+
162
+ self.blocks: dict[str, ScriptBlockData]
163
+ self.roots: dict[str, ScriptBlockData]
164
+
165
+ def __repr__(self) -> str:
166
+ return f"Dataset(manifest={self.manifest}, blocks={len(self.blocks)}, roots={len(self.roots)})"
167
+
168
+ def load(self, dataset_config: DatasetModel = DatasetModel()) -> None:
169
+ logging.info(f"Loading dataset... ({dataset_config})")
170
+ links = self.manifest.get_dataset_links(dataset_config)
171
+
172
+ logging.info(f"Downloading from following dataset links:")
173
+ logging.info(f"Blocks: {links['blocks']}")
174
+ logging.info(f"Roots: {links['roots']}")
175
+
176
+ # load datasets (possibly from cache)
177
+ blocks = load_json(links["blocks"])
178
+ roots = load_json(links["roots"])
179
+
180
+ logging.debug("Blocks JSON:")
181
+ logging.debug(blocks)
182
+ logging.debug("Roots JSON:")
183
+ logging.debug(roots)
184
+
185
+ # convert to proper type and store it
186
+ self.blocks = {k.lower(): ScriptBlockData(v) for k, v in blocks.items()}
187
+ self.roots = {k.lower(): ScriptBlockData(v) for k, v in roots.items()}
188
+
189
+ def test_for_root(self, path: Path) -> str | None:
190
+ """
191
+ Determine the root type for the given file path based on the dataset's root patterns.
192
+
193
+ Args:
194
+ path (Path): The file path to test against the dataset's root patterns.
195
+
196
+ Returns:
197
+ str | None: The name of the root type if a matching pattern is found, otherwise None.
198
+ """
199
+ # the path is used to determine the root type
200
+ # for that we need to resolve, normalize and use posix paths
201
+ resolved_path = path.resolve().as_posix()
202
+
203
+ # for each rootFile type, we test their identification patterns
204
+ # to determine if the document is a zedscript doc
205
+ for rootFile in self.roots.values():
206
+ patterns = rootFile.get('pattern', [])
207
+ for pattern in patterns:
208
+ regex = re.compile(pattern)
209
+ if regex.search(resolved_path) is not None:
210
+ return rootFile['name']
211
+ return None
212
+
213
+ def is_script_block(self, type: str) -> bool:
214
+ """
215
+ Determine if the given type corresponds to a script block in the dataset.
216
+
217
+ Args:
218
+ type (str): The type to check against the dataset's script blocks.
219
+
220
+ Returns:
221
+ bool: True if the type corresponds to a script block, False otherwise.
222
+ """
223
+ return type.lower() in self.blocks.keys()
224
+
225
+ def is_root(self, type: str) -> bool:
226
+ """
227
+ Determine if the given type corresponds to a root in the dataset.
228
+
229
+ Args:
230
+ type (str): The type to check against the dataset's roots.
231
+
232
+ Returns:
233
+ bool: True if the type corresponds to a root, False otherwise.
234
+ """
235
+ return type.lower() in self.roots.keys()
236
+
237
+ def get_script_block_data(self, type: str) -> ScriptBlockData:
238
+ """
239
+ Retrieve the script block data for the given type from the dataset.
240
+ You first need to verify that the type corresponds to a script block
241
+ or root in the dataset or the function will raise a ValueError.
242
+
243
+ Args:
244
+ type (str): The type of the script block or root to retrieve.
245
+
246
+ Raises:
247
+ ValueError: If the type does not correspond to a script block or root in the dataset.
248
+
249
+ Returns:
250
+ ScriptBlockData: The data associated with the specified script block or root type.
251
+ """
252
+ if self.is_script_block(type):
253
+ return self.blocks[type.lower()]
254
+ if self.is_root(type):
255
+ return self.roots[type.lower()]
256
+ raise ValueError(f"Script block data for type '{type}' not found")
257
+
258
+ def get_block_tree(self, type: str) -> list[str]:
259
+ """
260
+ Retrieve the variantOf tree of the specified script block type.
261
+
262
+ Args:
263
+ type (str): The type of the script block to retrieve the tree for.
264
+
265
+ Returns:
266
+ list[str]: A list representing the hierarchical tree of the script block type.
267
+ """
268
+ block_data = self.get_script_block_data(type)
269
+ assert block_data is not None, f"{type} block should be validated before retrieving its tree"
270
+
271
+ # if it's a variant of another block then include that
272
+ # block's tree before the current block's type in the tree
273
+ variant_of = block_data.get('variantOf')
274
+ variant_tree = [type]
275
+ if variant_of is not None:
276
+ variant_tree = self.get_block_tree(variant_of) + variant_tree
277
+
278
+ return variant_tree
279
+
280
+ def can_block_have_parameter(self, type: str, parameter: str) -> bool:
281
+ block_data = self.get_script_block_data(type)
282
+ assert block_data is not None, f"{type} block should be validated before validating parameters"
283
+ parameters = block_data.get('parameters', [])
284
+ return parameter.lower() in parameters.keys()
285
+
286
+ def get_parameter_data(self, type: str, parameter: str) -> ScriptBlockParameter:
287
+ block_data = self.get_script_block_data(type)
288
+ assert block_data is not None, f"{type} block should be validated before retrieving parameter data"
289
+ parameters = block_data.get('parameters', {})
290
+ return parameters[parameter.lower()]
291
+
292
+
293
+ def get_parameter_type(self, value: str, parameter_data: ScriptBlockParameter) -> ValueType:
294
+ """
295
+ Determine the type of a parameter value based on its content and
296
+ the expected type specified in the parameter data.
297
+
298
+ Args:
299
+ value (str): The value of the parameter to determine the type for.
300
+ parameter_data (ScriptBlockParameter): The metadata describing the expected type of the parameter.
301
+
302
+ Raises:
303
+ ValueError: If the expected type is an unsupported type from the dataset.
304
+
305
+ Returns:
306
+ ValueType: The determined type of the parameter value.
307
+ """
308
+ type_data = parameter_data.get('type')
309
+
310
+ # default to string in any case
311
+ if type_data is None:
312
+ return ValueType.STRING
313
+
314
+ expected_type = type_data['main']
315
+
316
+ # assert type is something we know about
317
+ if expected_type not in ValueType:
318
+ raise ValueError(f"Unknown parameter type '{expected_type}'")
319
+
320
+ return self.test_for_type(expected_type, value)
321
+
322
+ def test_for_type(self, expected_type: ValueType, value: str) -> ValueType:
323
+ # return early types we can't really determine from the value itself
324
+ if cant_self_validate(expected_type):
325
+ return expected_type
326
+
327
+ # check if boolean
328
+ if value.lower() in ["true", "false"]:
329
+ return ValueType.BOOLEAN
330
+
331
+ # check if float or integer
332
+ try:
333
+ float(value) # try to convert it
334
+
335
+ # it means that our value is a float or an integer
336
+ # so we need to distinguish between float and integer
337
+ if "." in value:
338
+ return ValueType.FLOAT
339
+
340
+ # if the expected type is float, then it's a float
341
+ # because floats are allowed to not contain dots
342
+ # (simply representing whole numbers)
343
+ elif expected_type == ValueType.FLOAT:
344
+ return ValueType.FLOAT
345
+
346
+ # else then we are simply an integer number
347
+ return ValueType.INTEGER
348
+ except ValueError:
349
+ pass
350
+
351
+ # default to string if no other type matches
352
+ return ValueType.STRING
353
+
354
+
355
+