pz-scripts-data 1.1.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.
- pz_scripts_data-1.1.0/LICENSE +4 -0
- pz_scripts_data-1.1.0/PKG-INFO +18 -0
- pz_scripts_data-1.1.0/README.md +41 -0
- pz_scripts_data-1.1.0/pyproject.toml +34 -0
- pz_scripts_data-1.1.0/setup.cfg +4 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data/blocks.py +65 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data/formatBlocks.py +279 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data/py.typed +0 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data/release.py +150 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data/schema.py +500 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data/sortItemParameters.py +42 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data/validateBlocks.py +139 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data.egg-info/PKG-INFO +18 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data.egg-info/SOURCES.txt +16 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data.egg-info/dependency_links.txt +1 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data.egg-info/entry_points.txt +2 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data.egg-info/requires.txt +3 -0
- pz_scripts_data-1.1.0/src/pz_scripts_data.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
PZ Scripts Data License
|
|
2
|
+
This dataset is provided as fully open-source. You are free to use, modify, repost this dataset and any tools in this projects. Any contribution to it is welcomed.
|
|
3
|
+
|
|
4
|
+
We are not responsible for what is made with this dataset, whenever it gets updated and breaks anything.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pz-scripts-data
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Dataset for the Scripts of Project Zomboid.
|
|
5
|
+
Author: SimKDT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Topic :: Database
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.12.3
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pyyaml
|
|
16
|
+
Requires-Dist: jsonschema
|
|
17
|
+
Requires-Dist: pydantic>=2.11
|
|
18
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# PZ Scripts Data
|
|
2
|
+
Provides information regarding various script blocks and parameters of Project Zomboid [scripts](https://pzwiki.net/wiki/Scripts) system. This dataset is used to provide a [ScriptsDocs](https://pz-wiki-modding.github.io/PZ-API-Docs/).
|
|
3
|
+
|
|
4
|
+
The translation data is now moved to [pz-translation-data](https://github.com/SirDoggyJvla/pz-translation-data) and are now deprecated. The main data files holding the last updated translation data are kept for tools that may use them, but will be removed in the future.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
PZ Scripts Data can be installed via PyPI, Git, or in development mode as described below.
|
|
9
|
+
|
|
10
|
+
<details open>
|
|
11
|
+
<summary>PyPI</summary>
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install pz-scripts-data
|
|
15
|
+
```
|
|
16
|
+
</details>
|
|
17
|
+
|
|
18
|
+
<details>
|
|
19
|
+
<summary>Git</summary>
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install git+https://github.com/PZ-Wiki-Modding/pz-scripts-data.git
|
|
23
|
+
```
|
|
24
|
+
</details>
|
|
25
|
+
|
|
26
|
+
<details>
|
|
27
|
+
<summary>Dev</summary>
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
</details>
|
|
33
|
+
|
|
34
|
+
## Schema
|
|
35
|
+
You can find a Pydantic model defining the data format in the [schema.py](src/pz_scripts_data/schema.py) file. A JSON schema file exported from this model is available in the [schemas/blocks.json](schemas/blocks.json) file.
|
|
36
|
+
|
|
37
|
+
## Contributing
|
|
38
|
+
This dataset is aimed to accept any contributions. See [CONTRIBUTING](CONTRIBUTING.md) for more information on how to contribute to this dataset.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
See [LICENSE](LICENSE) for more information.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pz-scripts-data"
|
|
3
|
+
description = "Dataset for the Scripts of Project Zomboid."
|
|
4
|
+
requires-python = ">=3.12.3"
|
|
5
|
+
version = "1.1.0"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "SimKDT" },
|
|
9
|
+
]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pyyaml",
|
|
12
|
+
"jsonschema",
|
|
13
|
+
"pydantic>=2.11",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Topic :: Database",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Development Status :: 5 - Production/Stable",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
release = "pz_scripts_data.release:main"
|
|
27
|
+
|
|
28
|
+
[tool.uv]
|
|
29
|
+
package = true
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
pz_scripts_data = [
|
|
33
|
+
"py.typed"
|
|
34
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
class BlockProperties(StrEnum):
|
|
4
|
+
VERSION = 'version'
|
|
5
|
+
REF_DESC = '#desc'
|
|
6
|
+
REF_FULL = '#ref'
|
|
7
|
+
DEPRECATED = 'deprecated'
|
|
8
|
+
|
|
9
|
+
NAME = 'name'
|
|
10
|
+
ID = 'ID'
|
|
11
|
+
SOFT_OVERRIDE = 'softOverride'
|
|
12
|
+
NO_COMMA = 'noComma'
|
|
13
|
+
PATTERN = 'pattern'
|
|
14
|
+
|
|
15
|
+
DESCRIPTION = 'description'
|
|
16
|
+
SHORT_DESCRIPTION = 'short_description'
|
|
17
|
+
|
|
18
|
+
VARIANT = 'variantOf'
|
|
19
|
+
VARIANTS = 'variants'
|
|
20
|
+
|
|
21
|
+
PARENTS = 'parents'
|
|
22
|
+
CHILDREN = 'children'
|
|
23
|
+
NEEDS_CHILDREN = 'needsChildren'
|
|
24
|
+
PARAMETERS = 'parameters'
|
|
25
|
+
PROPERTIES = 'properties'
|
|
26
|
+
|
|
27
|
+
IS_ROOT = 'isRoot'
|
|
28
|
+
|
|
29
|
+
class IDProperties(StrEnum):
|
|
30
|
+
OPTIONAL = 'optional'
|
|
31
|
+
CAN_HAVE_SPACES = 'canHaveSpace'
|
|
32
|
+
FORBIDDEN = 'forbidden'
|
|
33
|
+
|
|
34
|
+
VALUES = 'values'
|
|
35
|
+
PARENTS_WITHOUT = 'parentsWithout'
|
|
36
|
+
AS_TYPE = 'asType'
|
|
37
|
+
|
|
38
|
+
TRANSLATION = 'translation'
|
|
39
|
+
|
|
40
|
+
class ParameterProperties(StrEnum):
|
|
41
|
+
REF_DESC = '#desc'
|
|
42
|
+
REF_FULL = '#ref'
|
|
43
|
+
|
|
44
|
+
NAME = 'name'
|
|
45
|
+
DESCRIPTION = 'description'
|
|
46
|
+
|
|
47
|
+
TYPE = 'type'
|
|
48
|
+
DEFAULT = 'default'
|
|
49
|
+
REQUIRED = 'required'
|
|
50
|
+
CAN_BE_EMPTY = 'canBeEmpty'
|
|
51
|
+
MINIMUM = 'minimum'
|
|
52
|
+
MAXIMUM = 'maximum'
|
|
53
|
+
VALUES = 'values'
|
|
54
|
+
|
|
55
|
+
DEPRECATED = 'deprecated'
|
|
56
|
+
IS_USELESS = 'isUseless'
|
|
57
|
+
ALLOW_DUPLICATES = 'allowDuplicates'
|
|
58
|
+
INCOMPATIBLE_WITH = 'incompatibleWith'
|
|
59
|
+
|
|
60
|
+
SEE_ALSO = 'seeAlso'
|
|
61
|
+
|
|
62
|
+
class PropertyData(StrEnum):
|
|
63
|
+
NAME = 'name'
|
|
64
|
+
DESCRIPTION = 'description'
|
|
65
|
+
PROPERTIES = 'properties'
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import os, json, yaml
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import TypedDict, Optional, Any
|
|
4
|
+
|
|
5
|
+
from pz_scripts_data.blocks import BlockProperties, ParameterProperties, IDProperties, PropertyData
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type ScriptsBlock = dict[str, Any]
|
|
9
|
+
class Item(TypedDict):
|
|
10
|
+
input: Path
|
|
11
|
+
output: Path
|
|
12
|
+
data: Optional[dict[str, ScriptsBlock]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
items: list[Item] = [
|
|
16
|
+
Item(
|
|
17
|
+
input=Path("data/blocks"),
|
|
18
|
+
output=Path("out/scriptsBlocks.json"),
|
|
19
|
+
data=None,
|
|
20
|
+
),
|
|
21
|
+
Item(
|
|
22
|
+
input=Path("data/roots"),
|
|
23
|
+
output=Path("out/roots.json"),
|
|
24
|
+
data=None,
|
|
25
|
+
),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## PROCESS ITEM
|
|
33
|
+
|
|
34
|
+
def fetchBlocks(item: Item):
|
|
35
|
+
# combine all block json files into one
|
|
36
|
+
blocks: dict[str, ScriptsBlock] = {}
|
|
37
|
+
for file_path in item['input'].glob("*.yaml"):
|
|
38
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
39
|
+
data = yaml.safe_load(f)
|
|
40
|
+
key = data[ParameterProperties.NAME]
|
|
41
|
+
if key in blocks:
|
|
42
|
+
raise ValueError(f"Duplicate block name '{key}' found in '{file_path}'.")
|
|
43
|
+
blocks[key] = data
|
|
44
|
+
return blocks
|
|
45
|
+
|
|
46
|
+
def process_parameters(data: ScriptsBlock) -> ScriptsBlock:
|
|
47
|
+
# remove unnecessary fields
|
|
48
|
+
data.pop(BlockProperties.VERSION)
|
|
49
|
+
|
|
50
|
+
# replace parameters array into dictionary
|
|
51
|
+
# where keys are lowercased parameter names
|
|
52
|
+
parameters_data = data.get(BlockProperties.PARAMETERS, [])
|
|
53
|
+
parameters_new = {}
|
|
54
|
+
for param_data in parameters_data:
|
|
55
|
+
name = param_data[ParameterProperties.NAME]
|
|
56
|
+
parameters_new[name.lower()] = param_data
|
|
57
|
+
data[BlockProperties.PARAMETERS] = parameters_new
|
|
58
|
+
|
|
59
|
+
# handle properties
|
|
60
|
+
properties_data = data.get(BlockProperties.PROPERTIES, None)
|
|
61
|
+
if properties_data is not None:
|
|
62
|
+
for property_data in properties_data.values():
|
|
63
|
+
# transform into dictionary too
|
|
64
|
+
new_properties = {}
|
|
65
|
+
properties: list = property_data[PropertyData.PROPERTIES]
|
|
66
|
+
for prop_data in properties:
|
|
67
|
+
prop_name = prop_data[PropertyData.NAME]
|
|
68
|
+
new_properties[prop_name] = prop_data
|
|
69
|
+
property_data[PropertyData.PROPERTIES] = new_properties
|
|
70
|
+
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
# main
|
|
74
|
+
|
|
75
|
+
def process(item: Item):
|
|
76
|
+
blocks = fetchBlocks(item)
|
|
77
|
+
for key, block in blocks.items():
|
|
78
|
+
blocks[key] = process_parameters(block)
|
|
79
|
+
|
|
80
|
+
return blocks
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## PREPARE
|
|
85
|
+
|
|
86
|
+
# relationship and hierarchy
|
|
87
|
+
|
|
88
|
+
def add_source_ref_to_targets(
|
|
89
|
+
source_name: str,
|
|
90
|
+
targets_names: list[str],
|
|
91
|
+
items: list[Item],
|
|
92
|
+
array_type: BlockProperties):
|
|
93
|
+
"""
|
|
94
|
+
This is used to add source_name to the targets's array_type.
|
|
95
|
+
|
|
96
|
+
For example:
|
|
97
|
+
- source_name -> child_name
|
|
98
|
+
- targets_names -> child_parents
|
|
99
|
+
- array_type -> children (of parent)
|
|
100
|
+
will add source_name in the array stored at key array_type of the targets of targets_names.
|
|
101
|
+
(child_name --append--> array (key: children) of each parents)
|
|
102
|
+
"""
|
|
103
|
+
_processed: list[str] = []
|
|
104
|
+
for item in items:
|
|
105
|
+
blocks = item['data']
|
|
106
|
+
assert blocks is not None, "Something went wrong."
|
|
107
|
+
|
|
108
|
+
# add children_name ref to every parents
|
|
109
|
+
for parent_name in targets_names:
|
|
110
|
+
parent_block = blocks.get(parent_name, None)
|
|
111
|
+
if parent_block is None:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# init if needed
|
|
115
|
+
if array_type not in parent_block:
|
|
116
|
+
parent_block[array_type] = []
|
|
117
|
+
|
|
118
|
+
# add name
|
|
119
|
+
children: list[str] = parent_block[array_type]
|
|
120
|
+
if source_name not in children:
|
|
121
|
+
children.append(source_name)
|
|
122
|
+
_processed.append(parent_name)
|
|
123
|
+
|
|
124
|
+
# we make sure they were all properly processed
|
|
125
|
+
for parent_name in targets_names:
|
|
126
|
+
if parent_name not in _processed:
|
|
127
|
+
raise ValueError(f"Parent block '{parent_name}' not found for block '{source_name}'.")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def prepare_relationship(block: ScriptsBlock, items: list[Item]):
|
|
131
|
+
"""
|
|
132
|
+
This will retrieve the parents of this block and add a reference to itself inside the data,
|
|
133
|
+
of this parent block
|
|
134
|
+
"""
|
|
135
|
+
# handle parents
|
|
136
|
+
parents = block.get(BlockProperties.PARENTS, [])
|
|
137
|
+
if parents:
|
|
138
|
+
add_source_ref_to_targets(
|
|
139
|
+
block[BlockProperties.NAME], parents, items,
|
|
140
|
+
BlockProperties.CHILDREN)
|
|
141
|
+
|
|
142
|
+
# handle variants
|
|
143
|
+
variant = block.get(BlockProperties.VARIANT, None)
|
|
144
|
+
if variant:
|
|
145
|
+
add_source_ref_to_targets(
|
|
146
|
+
block[BlockProperties.NAME], [variant], items,
|
|
147
|
+
BlockProperties.VARIANTS)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# description and reference
|
|
151
|
+
|
|
152
|
+
def find_block_in_items(block_name: str, items: list[Item]) -> ScriptsBlock | None:
|
|
153
|
+
for item in items:
|
|
154
|
+
blocks = item['data']
|
|
155
|
+
assert blocks is not None, "Something is wrong."
|
|
156
|
+
|
|
157
|
+
if block_name in blocks:
|
|
158
|
+
return blocks[block_name]
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def copy_block_ref(block: ScriptsBlock, items: list[Item]) -> ScriptsBlock:
|
|
163
|
+
"""
|
|
164
|
+
#desc copies the description of the source block
|
|
165
|
+
#ref copies every key values of that source block into the block (if not already present)
|
|
166
|
+
"""
|
|
167
|
+
# copy #desc
|
|
168
|
+
if BlockProperties.REF_DESC in block:
|
|
169
|
+
desc_source_key: str = block[BlockProperties.REF_DESC]
|
|
170
|
+
desc_source = find_block_in_items(desc_source_key, items)
|
|
171
|
+
|
|
172
|
+
if desc_source is not None:
|
|
173
|
+
block[BlockProperties.DESCRIPTION] = desc_source[BlockProperties.DESCRIPTION]
|
|
174
|
+
|
|
175
|
+
# copy the short description if available too
|
|
176
|
+
if BlockProperties.SHORT_DESCRIPTION in desc_source:
|
|
177
|
+
block[BlockProperties.SHORT_DESCRIPTION] = desc_source[BlockProperties.SHORT_DESCRIPTION]
|
|
178
|
+
|
|
179
|
+
# copy #ref
|
|
180
|
+
if BlockProperties.REF_FULL in block:
|
|
181
|
+
ref_source_key: str = block[BlockProperties.REF_FULL]
|
|
182
|
+
ref_source = find_block_in_items(ref_source_key, items)
|
|
183
|
+
if ref_source is not None:
|
|
184
|
+
# we copy every key-value from the ref_block
|
|
185
|
+
# that doesn't already exist in the block
|
|
186
|
+
for key, value in ref_source.items():
|
|
187
|
+
if key not in block:
|
|
188
|
+
block[key] = value
|
|
189
|
+
|
|
190
|
+
return block
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# parameters
|
|
194
|
+
|
|
195
|
+
def get_source_parameter(ref_key: str, items: list[Item]) -> dict:
|
|
196
|
+
"""
|
|
197
|
+
This will return the source parameter data from the ref_key.
|
|
198
|
+
The ref_key is in the format "BlockName/ParameterName"
|
|
199
|
+
"""
|
|
200
|
+
# find the source block and parameter
|
|
201
|
+
path = ref_key.split('/')
|
|
202
|
+
assert len(path) == 2, f"Invalid #ref format for '{ref_key}'. Needs to be in format 'BlockName/ParameterName'."
|
|
203
|
+
origin_block = path[0]
|
|
204
|
+
origin_param = path[1].lower()
|
|
205
|
+
|
|
206
|
+
origin_block_data = find_block_in_items(origin_block, items)
|
|
207
|
+
|
|
208
|
+
# make sure the source block and parameter exist
|
|
209
|
+
if origin_block_data is None:
|
|
210
|
+
raise ValueError(f"Source block '{origin_block}' not found for parameter '{origin_param}'.")
|
|
211
|
+
if BlockProperties.PARAMETERS not in origin_block_data:
|
|
212
|
+
raise ValueError(f"Source block '{origin_block}' has no parameters for parameter '{origin_param}'.")
|
|
213
|
+
if origin_param not in origin_block_data[BlockProperties.PARAMETERS]:
|
|
214
|
+
raise ValueError(f"Source block '{origin_block}' has no parameter '{origin_param}'.")
|
|
215
|
+
|
|
216
|
+
return origin_block_data[BlockProperties.PARAMETERS][origin_param]
|
|
217
|
+
|
|
218
|
+
def copy_parameters_ref(parameters: dict[str, dict], items: list[Item]):
|
|
219
|
+
for param_key, param_data in parameters.items():
|
|
220
|
+
if ParameterProperties.REF_FULL in param_data:
|
|
221
|
+
ref_key = param_data[ParameterProperties.REF_FULL]
|
|
222
|
+
|
|
223
|
+
source_param_data = get_source_parameter(ref_key, items)
|
|
224
|
+
assert source_param_data is not None, f"Source parameter '{ref_key}' not found for parameter '{param_key}'."
|
|
225
|
+
|
|
226
|
+
# copy every key-value from the source parameter data
|
|
227
|
+
# that doesn't already exist in the parameter data
|
|
228
|
+
for key, value in source_param_data.items():
|
|
229
|
+
if key not in param_data:
|
|
230
|
+
param_data[key] = value
|
|
231
|
+
|
|
232
|
+
if ParameterProperties.REF_DESC in param_data:
|
|
233
|
+
desc_key = param_data[ParameterProperties.REF_DESC]
|
|
234
|
+
|
|
235
|
+
source_param_data = get_source_parameter(desc_key, items)
|
|
236
|
+
assert source_param_data is not None, f"Source parameter '{desc_key}' not found for parameter '{param_key}'."
|
|
237
|
+
|
|
238
|
+
# make sure the source parameter has a description
|
|
239
|
+
if ParameterProperties.DESCRIPTION not in source_param_data:
|
|
240
|
+
raise ValueError(f"Parameter '{param_key}' has #desc but source has no description.")
|
|
241
|
+
|
|
242
|
+
# only copy the description
|
|
243
|
+
param_data[ParameterProperties.DESCRIPTION] = source_param_data[ParameterProperties.DESCRIPTION]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# main
|
|
247
|
+
|
|
248
|
+
def prepare(item: Item, items: list[Item]):
|
|
249
|
+
blocks = item['data']
|
|
250
|
+
assert blocks is not None, "Something went wrong."
|
|
251
|
+
|
|
252
|
+
for block in blocks.values():
|
|
253
|
+
prepare_relationship(block, items)
|
|
254
|
+
copy_block_ref(block, items)
|
|
255
|
+
|
|
256
|
+
parameters = block.get(BlockProperties.PARAMETERS, {})
|
|
257
|
+
copy_parameters_ref(parameters, items)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
return blocks
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main():
|
|
268
|
+
for item in items:
|
|
269
|
+
item['data'] = process(item)
|
|
270
|
+
for item in items:
|
|
271
|
+
item['data'] = prepare(item, items)
|
|
272
|
+
for item in items:
|
|
273
|
+
output_file = item['output']
|
|
274
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
276
|
+
json.dump(item['data'], f, indent=2, ensure_ascii=False, sort_keys=True)
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import datetime
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
def assert_release_number(version: str) -> tuple[int, int, int]:
|
|
8
|
+
splitted = version.split('.')
|
|
9
|
+
if len(splitted) != 3:
|
|
10
|
+
raise ValueError("Invalid build version format. Expected format: 'major.minor.patch'")
|
|
11
|
+
try:
|
|
12
|
+
major, minor, patch = [int(x) for x in splitted]
|
|
13
|
+
except ValueError:
|
|
14
|
+
raise ValueError("Invalid build version format. Expected format: 'major.minor.patch' with integers")
|
|
15
|
+
except Exception as e:
|
|
16
|
+
raise ValueError(f"Error parsing build version: {e}")
|
|
17
|
+
return major, minor, patch
|
|
18
|
+
|
|
19
|
+
def assert_release_with_version(version: str) -> tuple[int, int, int, int]:
|
|
20
|
+
splitted = version.split('.')
|
|
21
|
+
if len(splitted) != 4:
|
|
22
|
+
raise ValueError("Invalid release version format. Expected format: 'major.minor.patch.version'")
|
|
23
|
+
try:
|
|
24
|
+
major, minor, patch, release_version = [int(x) for x in splitted]
|
|
25
|
+
except ValueError:
|
|
26
|
+
raise ValueError("Invalid release version format. Expected format: 'major.minor.patch.version' with integers")
|
|
27
|
+
except Exception as e:
|
|
28
|
+
raise ValueError(f"Error parsing release version: {e}")
|
|
29
|
+
return major, minor, patch, release_version
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
parser = argparse.ArgumentParser(description="Release a dataset build")
|
|
33
|
+
parser.add_argument("--set-stable", type=str,
|
|
34
|
+
help="Move the stable to the provided build version")
|
|
35
|
+
parser.add_argument("--set-latest-build", type=str,
|
|
36
|
+
help="Update the latest build version in the manifest")
|
|
37
|
+
parser.add_argument("--set-latest-release", type=str,
|
|
38
|
+
help="Update the latest release version in the manifest")
|
|
39
|
+
parser.add_argument("--build", type=str,
|
|
40
|
+
help="Specify the new build version")
|
|
41
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
42
|
+
help="Perform a dry run without making any changes")
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
|
|
45
|
+
# retrieve manifest file
|
|
46
|
+
WORKSPACE = Path(__file__).parent.parent.parent
|
|
47
|
+
MANIFEST_FILE = WORKSPACE / "manifest.json"
|
|
48
|
+
assert MANIFEST_FILE.exists(), f"Manifest file not found at {MANIFEST_FILE}"
|
|
49
|
+
|
|
50
|
+
# retrieve the latest manifest data
|
|
51
|
+
MANIFEST_DATA = json.loads(MANIFEST_FILE.read_text())
|
|
52
|
+
latest_build = MANIFEST_DATA['latest_build']
|
|
53
|
+
major, minor, patch = assert_release_number(latest_build)
|
|
54
|
+
|
|
55
|
+
# handle set-latest-build argument
|
|
56
|
+
set_latest_build = args.set_latest_build
|
|
57
|
+
set_latest_release = args.set_latest_release
|
|
58
|
+
set_stable = args.set_stable
|
|
59
|
+
dry_run = args.dry_run
|
|
60
|
+
|
|
61
|
+
pushed_new_release = None
|
|
62
|
+
if set_latest_build is not None:
|
|
63
|
+
print(f"Setting latest build to {set_latest_build}")
|
|
64
|
+
splitted = set_latest_build.split('.')
|
|
65
|
+
if len(splitted) != 3:
|
|
66
|
+
raise ValueError("Invalid build version format. Expected format: 'major.minor.patch'")
|
|
67
|
+
assert_release_number(set_latest_build)
|
|
68
|
+
|
|
69
|
+
# set new version in the manifest
|
|
70
|
+
MANIFEST_DATA['latest_build'] = set_latest_build
|
|
71
|
+
|
|
72
|
+
# set new stable
|
|
73
|
+
elif set_stable:
|
|
74
|
+
print(f"Setting stable to {set_stable}")
|
|
75
|
+
assert_release_with_version(set_stable)
|
|
76
|
+
MANIFEST_DATA['stable'] = set_stable
|
|
77
|
+
|
|
78
|
+
# handle set-latest-release argument
|
|
79
|
+
elif set_latest_release is not None:
|
|
80
|
+
print(f"Setting latest release to {set_latest_release}")
|
|
81
|
+
assert_release_with_version(set_latest_release)
|
|
82
|
+
|
|
83
|
+
# get associated release
|
|
84
|
+
releases = MANIFEST_DATA['releases']
|
|
85
|
+
if set_latest_release not in releases.keys():
|
|
86
|
+
raise ValueError(f"Release version {set_latest_release} not found in manifest")
|
|
87
|
+
MANIFEST_DATA['latest_release'] = set_latest_release
|
|
88
|
+
|
|
89
|
+
# no args, push a new release
|
|
90
|
+
else:
|
|
91
|
+
print("Pushing a new release")
|
|
92
|
+
current_date = datetime.datetime.now().isoformat()
|
|
93
|
+
releases = MANIFEST_DATA['releases']
|
|
94
|
+
build = args.build if args.build is not None else f"{major}.{minor}.{patch}"
|
|
95
|
+
version = 0
|
|
96
|
+
if build not in releases.keys():
|
|
97
|
+
releases[build] = {
|
|
98
|
+
'date': current_date,
|
|
99
|
+
'version': version,
|
|
100
|
+
}
|
|
101
|
+
else:
|
|
102
|
+
existing_build = releases[build]
|
|
103
|
+
existing_build['date'] = current_date
|
|
104
|
+
existing_build['version'] += 1
|
|
105
|
+
version = existing_build['version']
|
|
106
|
+
pushed_new_release = f"{build}.{version}"
|
|
107
|
+
|
|
108
|
+
# write latest changes
|
|
109
|
+
if not dry_run:
|
|
110
|
+
MANIFEST_FILE.write_text(json.dumps(MANIFEST_DATA, indent=4))
|
|
111
|
+
|
|
112
|
+
# commit latest manifest changes
|
|
113
|
+
try:
|
|
114
|
+
if not dry_run:
|
|
115
|
+
subprocess.run(["git", "add", str(MANIFEST_FILE)], check=True)
|
|
116
|
+
message = "manifest"
|
|
117
|
+
if pushed_new_release is not None:
|
|
118
|
+
message += f": {pushed_new_release}"
|
|
119
|
+
elif set_stable is not None:
|
|
120
|
+
message += f": New stable {set_stable}"
|
|
121
|
+
elif set_latest_release is not None:
|
|
122
|
+
message += f": New latest release {set_latest_release}"
|
|
123
|
+
elif set_latest_build is not None:
|
|
124
|
+
message += f": New latest build {set_latest_build}"
|
|
125
|
+
subprocess.run(["git", "commit", "-m", message], check=True)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f"Error committing changes: {e}")
|
|
128
|
+
|
|
129
|
+
# that one we don't ignore, since that could been connection issues
|
|
130
|
+
if not dry_run:
|
|
131
|
+
subprocess.run(["git", "push"], check=True)
|
|
132
|
+
|
|
133
|
+
# if set as stable is active
|
|
134
|
+
# move stable tag
|
|
135
|
+
if set_stable is not None:
|
|
136
|
+
if not dry_run:
|
|
137
|
+
subprocess.run(["git", "tag", "-fa", "stable", "-m", "Moved stable tag"], check=True)
|
|
138
|
+
subprocess.run(["git", "push", "-f", "--tags"], check=True)
|
|
139
|
+
print(f"Moved stable tag")
|
|
140
|
+
|
|
141
|
+
if pushed_new_release is not None:
|
|
142
|
+
if not dry_run:
|
|
143
|
+
subprocess.run(["git", "tag", "-fa", pushed_new_release, "-m", "New release"], check=True)
|
|
144
|
+
subprocess.run(["git", "push", "-f", "--tags"], check=True)
|
|
145
|
+
print(f"Pushed new release: {pushed_new_release}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""Pydantic models defining the structure of block definition files.
|
|
2
|
+
|
|
3
|
+
``BlockDefinition`` is the root model. Its JSON Schema is exported to
|
|
4
|
+
``schemas/blocks.json`` by running ``python -m pz_scripts_data.schema``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal, Self
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
13
|
+
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
|
|
14
|
+
from pydantic_core import core_schema
|
|
15
|
+
|
|
16
|
+
DEFAULT_OUTPUT = Path(__file__).resolve().parents[2] / "schemas" / "blocks.json"
|
|
17
|
+
|
|
18
|
+
ScalarType = Literal["string", "float", "boolean", "integer"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SchemaModel(BaseModel):
|
|
22
|
+
"""Base model. Unknown keys are rejected and no value may be null."""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(extra="forbid", strict=True, validate_by_name=True)
|
|
25
|
+
|
|
26
|
+
@model_validator(mode="before")
|
|
27
|
+
@classmethod
|
|
28
|
+
def _reject_null(cls, data: Any) -> Any:
|
|
29
|
+
if isinstance(data, dict) and any(value is None for value in data.values()):
|
|
30
|
+
raise ValueError("null is not allowed; omit the key instead")
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BlockReference(SchemaModel):
|
|
35
|
+
"""If the parameter is of type 'block', this can be used to provide a description of the expected format of the block."""
|
|
36
|
+
|
|
37
|
+
name: str = Field(description="The name of the block type that this parameter references to.")
|
|
38
|
+
full_type: bool = Field(
|
|
39
|
+
True,
|
|
40
|
+
alias="fullType",
|
|
41
|
+
description="Indicates whether the parameter's value can match the module and block name, for example 'module.block'.",
|
|
42
|
+
)
|
|
43
|
+
no_auto_import: bool = Field(
|
|
44
|
+
False,
|
|
45
|
+
alias="noAutoImport",
|
|
46
|
+
description="Indicates whenever this parameter can automatically check its own parent block module when fullType is set to true. If false, this means that the module won't be automatically imported.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TranslationDefinition(SchemaModel):
|
|
51
|
+
"""If the parameter is of type 'translation', this can be used to provide a description of the expected format of the translation. For script blocks, this is associated to the ID of the block."""
|
|
52
|
+
|
|
53
|
+
key_pattern: str = Field(
|
|
54
|
+
alias="keyPattern",
|
|
55
|
+
description="A regex pattern that the translation key in the source must match. The '{value}' placeholder is replaced by the ID of the block or value of the parameter. '{module}' is replaced by the module of the block.",
|
|
56
|
+
)
|
|
57
|
+
source_file: str = Field(
|
|
58
|
+
alias="sourceFile",
|
|
59
|
+
description="The source file where the translation key is defined (without the .json extension).",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BlockID(SchemaModel):
|
|
64
|
+
"""Defines the ID properties for this block type. If not specified, the block type does not have an ID."""
|
|
65
|
+
|
|
66
|
+
parents_without: list[str] | None = Field(
|
|
67
|
+
None,
|
|
68
|
+
alias="parentsWithout",
|
|
69
|
+
description="A list of block names that cannot be parents if this block has an ID.",
|
|
70
|
+
)
|
|
71
|
+
values: list[str] | None = Field(
|
|
72
|
+
None,
|
|
73
|
+
description="A list of allowed ID values for this block. If not given, any value is allowed.",
|
|
74
|
+
)
|
|
75
|
+
as_type: bool | None = Field(
|
|
76
|
+
None,
|
|
77
|
+
alias="asType",
|
|
78
|
+
description="Specifies if the ID of the block should count as the block type. For example, components have specific definitions based on their ID. This means a script block needs to be defined with the name 'component <componentID>' as well as 'component' to cover all component types available with the 'values' attribute.",
|
|
79
|
+
)
|
|
80
|
+
optional: list[str] | None = Field(
|
|
81
|
+
None,
|
|
82
|
+
description="A list of ID values that are optional for this block type.",
|
|
83
|
+
)
|
|
84
|
+
can_have_space: bool | None = Field(
|
|
85
|
+
None,
|
|
86
|
+
alias="canHaveSpace",
|
|
87
|
+
description="Indicates whether the ID of the block can contain spaces. If false or not specified, the ID cannot contain spaces.",
|
|
88
|
+
)
|
|
89
|
+
forbidden: list[str] | None = Field(
|
|
90
|
+
None,
|
|
91
|
+
description="A list of forbidden ID values for this block. If not provided, no values are forbidden.",
|
|
92
|
+
)
|
|
93
|
+
translation: TranslationDefinition | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BlockDeprecation(SchemaModel):
|
|
97
|
+
"""Indicates whether the block type is deprecated."""
|
|
98
|
+
|
|
99
|
+
replaced_by: str | None = Field(
|
|
100
|
+
None,
|
|
101
|
+
alias="replacedBy",
|
|
102
|
+
description="The name of the block type that replaces this deprecated block type.",
|
|
103
|
+
)
|
|
104
|
+
description: str | None = Field(
|
|
105
|
+
None,
|
|
106
|
+
description="A brief description of why the block type is deprecated and any additional information about the deprecation.",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _ArrayBase(SchemaModel):
|
|
111
|
+
separator: str = Field(description="Specifies the separator used to split the string into array items.")
|
|
112
|
+
min_items: float | None = Field(
|
|
113
|
+
None,
|
|
114
|
+
alias="min",
|
|
115
|
+
description="The minimum number of items required in the array.",
|
|
116
|
+
)
|
|
117
|
+
max_items: float | None = Field(
|
|
118
|
+
None,
|
|
119
|
+
alias="max",
|
|
120
|
+
description="The maximum number of items allowed in the array.",
|
|
121
|
+
)
|
|
122
|
+
soft_override: bool | None = Field(
|
|
123
|
+
None,
|
|
124
|
+
alias="softOverride",
|
|
125
|
+
description="If true, a soft override of this property will add its content to the previously already existing array. Thus adding the old entries is not necessary and should be avoided to prevent duplicates.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ScalarArrayDefinition(_ArrayBase):
|
|
130
|
+
item_type: ScalarType = Field(alias="type", description="Specifies the data type of the items in the array.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BlockArrayDefinition(_ArrayBase):
|
|
134
|
+
item_type: Literal["block"] = Field(alias="type", description="Specifies the data type of the items in the array.")
|
|
135
|
+
block: BlockReference = Field(description="The block type of the items in the array.")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
ArrayDefinition = ScalarArrayDefinition | BlockArrayDefinition
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _ObjectBase(SchemaModel):
|
|
142
|
+
key_value_separator: str = Field(
|
|
143
|
+
alias="keyValueSeparator",
|
|
144
|
+
description="Specifies the separator used to split the key and the value in the key-value pairs. The parameter 'pairsSeparator' is used to split the pairs from each others.",
|
|
145
|
+
)
|
|
146
|
+
pairs_separator: str = Field(
|
|
147
|
+
alias="pairsSeparator",
|
|
148
|
+
description="Specifies the separator used to split the pairs from each others. The parameter 'keyValueSeparator' is used to split the keys and values.",
|
|
149
|
+
)
|
|
150
|
+
soft_override: bool | None = Field(
|
|
151
|
+
None,
|
|
152
|
+
alias="softOverride",
|
|
153
|
+
description="If true, a soft override of this property will add its content to the previously already existing array. Thus adding the old entries is not necessary and should be avoided to prevent duplicates.",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ScalarKeyScalarValueObject(_ObjectBase):
|
|
158
|
+
key_type: ScalarType = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
|
|
159
|
+
value_type: ScalarType = Field(alias="valueType", description="Specifies the data type of the values in the object.")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class BlockKeyBlockValueObject(_ObjectBase):
|
|
163
|
+
key_type: Literal["block"] = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
|
|
164
|
+
key_block: BlockReference = Field(alias="keyBlock", description="The block type of the keys in the object.")
|
|
165
|
+
value_type: Literal["block"] = Field(alias="valueType", description="Specifies the data type of the values in the object.")
|
|
166
|
+
value_block: BlockReference = Field(alias="valueBlock", description="The block type of the values in the object.")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class BlockKeyScalarValueObject(_ObjectBase):
|
|
170
|
+
key_type: Literal["block"] = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
|
|
171
|
+
key_block: BlockReference = Field(alias="keyBlock", description="The block type of the keys in the object.")
|
|
172
|
+
value_type: ScalarType = Field(alias="valueType", description="Specifies the data type of the values in the object.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ScalarKeyBlockValueObject(_ObjectBase):
|
|
176
|
+
key_type: ScalarType = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
|
|
177
|
+
value_type: Literal["block"] = Field(alias="valueType", description="Specifies the data type of the values in the object.")
|
|
178
|
+
value_block: BlockReference = Field(alias="valueBlock", description="The block type of the values in the object.")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
ObjectDefinition = (
|
|
182
|
+
ScalarKeyScalarValueObject
|
|
183
|
+
| BlockKeyBlockValueObject
|
|
184
|
+
| BlockKeyScalarValueObject
|
|
185
|
+
| ScalarKeyBlockValueObject
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class CallbackParameter(SchemaModel):
|
|
190
|
+
name: str = Field(description="The name of the parameter.")
|
|
191
|
+
param_type: str = Field(alias="type", description="The type of the parameter.")
|
|
192
|
+
is_java: bool = Field(
|
|
193
|
+
False,
|
|
194
|
+
alias="isJava",
|
|
195
|
+
description="Indicates whether the parameter is a Java object.",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class CallbackDefinition(SchemaModel):
|
|
200
|
+
"""Indicates whether the parameter is a callback function."""
|
|
201
|
+
|
|
202
|
+
parameters: list[CallbackParameter] = Field(description="A list of parameters that the callback function takes.")
|
|
203
|
+
returns: str | None = Field(None, description="The return type of the callback function.")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ArrayParameterType(SchemaModel):
|
|
207
|
+
main: Literal["array"] = Field(description="The main data type of the parameter.")
|
|
208
|
+
array: ArrayDefinition = Field(
|
|
209
|
+
description="If the parameter is of type 'array', this can be used to provide a description of the expected format of the array.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ObjectParameterType(SchemaModel):
|
|
214
|
+
main: Literal["object"] = Field(description="The main data type of the parameter.")
|
|
215
|
+
object_: ObjectDefinition = Field(
|
|
216
|
+
alias="object",
|
|
217
|
+
description="If the parameter is of type 'object', this can be used to provide a description of the expected format of the object.",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class BlockParameterType(SchemaModel):
|
|
222
|
+
main: Literal["block"] = Field(description="The main data type of the parameter.")
|
|
223
|
+
block: BlockReference = Field(
|
|
224
|
+
description="If the parameter is of type 'block', this can be used to provide a description of the expected format of the block.",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class CallbackParameterType(SchemaModel):
|
|
229
|
+
main: Literal["callback"] = Field(description="The main data type of the parameter.")
|
|
230
|
+
callback: CallbackDefinition = Field(description="Indicates whether the parameter is a callback function.")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class TranslationParameterType(SchemaModel):
|
|
234
|
+
main: Literal["translation"] = Field(description="The main data type of the parameter.")
|
|
235
|
+
translation: TranslationDefinition = Field(
|
|
236
|
+
description="If the parameter is of type 'translation', this can be used to provide a description of the expected format of the translation.",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ScalarParameterType(SchemaModel):
|
|
241
|
+
main: ScalarType = Field(description="The main data type of the parameter.")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
ParameterType = (
|
|
245
|
+
ArrayParameterType
|
|
246
|
+
| ObjectParameterType
|
|
247
|
+
| BlockParameterType
|
|
248
|
+
| CallbackParameterType
|
|
249
|
+
| TranslationParameterType
|
|
250
|
+
| ScalarParameterType
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ParameterDeprecation(SchemaModel):
|
|
255
|
+
"""Indicates whether the parameter is deprecated."""
|
|
256
|
+
|
|
257
|
+
replaced_by: str | None = Field(
|
|
258
|
+
None,
|
|
259
|
+
alias="replacedBy",
|
|
260
|
+
description="The name of the parameter that replaces this deprecated parameter.",
|
|
261
|
+
)
|
|
262
|
+
description: str | None = Field(
|
|
263
|
+
None,
|
|
264
|
+
description="A brief description of why the parameter is deprecated and any additional information about the deprecation.",
|
|
265
|
+
)
|
|
266
|
+
version: str | None = Field(None, description="The version(s) in which the parameter is deprecated.")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ParameterNeed(SchemaModel):
|
|
270
|
+
"""A condition on another parameter that must be met for this parameter to be valid."""
|
|
271
|
+
|
|
272
|
+
name: str | None = Field(None, description="The name of the parameter that this condition depends on.")
|
|
273
|
+
values: list[str | float | bool] | None = Field(
|
|
274
|
+
None,
|
|
275
|
+
description="The values of the dependent parameter that must be met for this parameter to be valid.",
|
|
276
|
+
)
|
|
277
|
+
value_to_type: dict[str, str] | None = Field(
|
|
278
|
+
None,
|
|
279
|
+
alias="valueToType",
|
|
280
|
+
description="A mapping of values of the dependent parameter to the expected type of this parameter. For example, if the dependent parameter is 'type' and it has a value 'integer', then this parameter should be of type 'float'. The key is the value of the dependent parameter, and the value is the expected type of this parameter when the dependent parameter has that value.",
|
|
281
|
+
examples=[{"integer": "float", "double": "float", "boolean": "boolean", "string": "string"}],
|
|
282
|
+
)
|
|
283
|
+
minimum: float | None = Field(
|
|
284
|
+
None,
|
|
285
|
+
description="The minimum value for this parameter when the dependent parameter has a specific value. Requires the target parameter to be of type 'float' or 'integer'.",
|
|
286
|
+
)
|
|
287
|
+
maximum: float | None = Field(
|
|
288
|
+
None,
|
|
289
|
+
description="The maximum value for this parameter when the dependent parameter has a specific value. Requires the target parameter to be of type 'float' or 'integer'.",
|
|
290
|
+
)
|
|
291
|
+
children: list[str] | None = Field(
|
|
292
|
+
None,
|
|
293
|
+
description="A list of child blocks that must be present for this parameter to be valid.",
|
|
294
|
+
)
|
|
295
|
+
parents: list[str] | None = Field(
|
|
296
|
+
None,
|
|
297
|
+
description="A list of parent blocks that must be present for this parameter to be valid.",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ParameterDefinition(SchemaModel):
|
|
302
|
+
"""A parameter available for a block type."""
|
|
303
|
+
|
|
304
|
+
name: str | None = Field(None, description="The name of the parameter.")
|
|
305
|
+
any_name: bool | None = Field(
|
|
306
|
+
None,
|
|
307
|
+
alias="anyName",
|
|
308
|
+
description="A parameter with `anyName` can have any name.",
|
|
309
|
+
)
|
|
310
|
+
ref: str | None = Field(None, alias="#ref", description="A reference to another block parameter.")
|
|
311
|
+
ref_desc: str | None = Field(
|
|
312
|
+
None,
|
|
313
|
+
alias="#desc",
|
|
314
|
+
description="A reference to the description of another block parameter. This allows sharing the same description between multiple parameters without using #ref, which would also share other properties.",
|
|
315
|
+
)
|
|
316
|
+
description: str | None = Field(None, description="A brief description of what the parameter does.")
|
|
317
|
+
allowed_duplicate: bool | None = Field(
|
|
318
|
+
None,
|
|
319
|
+
alias="allowedDuplicate",
|
|
320
|
+
description="Indicates whether the parameter can be duplicated.",
|
|
321
|
+
)
|
|
322
|
+
can_be_empty: bool | None = Field(
|
|
323
|
+
None,
|
|
324
|
+
alias="canBeEmpty",
|
|
325
|
+
description="Indicates whether the parameter can be empty.",
|
|
326
|
+
)
|
|
327
|
+
item_types: list[str] | None = Field(
|
|
328
|
+
None,
|
|
329
|
+
alias="itemTypes",
|
|
330
|
+
description="DEPRECATED: Specifies the type of item the parameter is associated with (e.g., 'vehicle', 'item', etc.). Only for item scripts.",
|
|
331
|
+
)
|
|
332
|
+
parents_only: list[str] | None = Field(
|
|
333
|
+
None,
|
|
334
|
+
alias="parentsOnly",
|
|
335
|
+
description="DEPRECATED: A list of parent block types for which this parameter is applicable.",
|
|
336
|
+
)
|
|
337
|
+
required: bool | None = Field(None, description="Indicates whether the parameter is required.")
|
|
338
|
+
default_value: str | float | bool | list[Any] | dict[str, Any] | None = Field(
|
|
339
|
+
None,
|
|
340
|
+
alias="default",
|
|
341
|
+
description="The default value for the parameter if not specified.",
|
|
342
|
+
)
|
|
343
|
+
minimum: float | None = Field(
|
|
344
|
+
None,
|
|
345
|
+
description="The minimum value for the parameter if it is a number. Needs type.main to be 'float' or 'integer' to be applicable.",
|
|
346
|
+
)
|
|
347
|
+
maximum: float | None = Field(
|
|
348
|
+
None,
|
|
349
|
+
description="The maximum value for the parameter if it is a number. Needs type.main to be 'float' or 'integer' to be applicable.",
|
|
350
|
+
)
|
|
351
|
+
data_type: ParameterType | None = Field(
|
|
352
|
+
None,
|
|
353
|
+
alias="type",
|
|
354
|
+
description="The data type of the parameter.",
|
|
355
|
+
)
|
|
356
|
+
values: list[str | float] | None = Field(None, description="A list of allowed values for the parameter.")
|
|
357
|
+
deprecated: ParameterDeprecation | None = None
|
|
358
|
+
needs: list[ParameterNeed] | None = Field(
|
|
359
|
+
None,
|
|
360
|
+
description="A list of conditions of other parameters that must be met for this parameter to be valid.",
|
|
361
|
+
)
|
|
362
|
+
incompatible_with: list[str] | None = Field(
|
|
363
|
+
None,
|
|
364
|
+
alias="incompatibleWith",
|
|
365
|
+
description="List of other parameters which are incompatible with this one.",
|
|
366
|
+
)
|
|
367
|
+
is_useless: bool = Field(
|
|
368
|
+
False,
|
|
369
|
+
alias="isUseless",
|
|
370
|
+
description="Indicates whether the parameter is considered useless or not. This can be used to flag parameters that have no effect in the game. Not to be confused with deprecated which indicates a parameter that previously worked but no longer does.",
|
|
371
|
+
)
|
|
372
|
+
see_also: list[str] | None = Field(
|
|
373
|
+
None,
|
|
374
|
+
alias="seeAlso",
|
|
375
|
+
description="A list of other parameters that are related to this one.",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class BlockDefinition(SchemaModel):
|
|
380
|
+
"""Schema of a block definition file."""
|
|
381
|
+
|
|
382
|
+
model_config = ConfigDict(
|
|
383
|
+
title="Blocks Folder General Schema",
|
|
384
|
+
json_schema_extra=lambda schema: schema.update(
|
|
385
|
+
allOf=[
|
|
386
|
+
{
|
|
387
|
+
"if": {"required": ["isRoot"], "properties": {"isRoot": {"const": True}}},
|
|
388
|
+
"then": {"required": ["pattern"]},
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
version: float | str | None = None
|
|
395
|
+
ref_desc: str | None = Field(
|
|
396
|
+
None,
|
|
397
|
+
alias="#desc",
|
|
398
|
+
description="A reference to the description of another block type. This allows sharing the same description between multiple block types without using #ref, which would also share other properties.",
|
|
399
|
+
)
|
|
400
|
+
ref: str | None = Field(
|
|
401
|
+
None,
|
|
402
|
+
alias="#ref",
|
|
403
|
+
description="A reference to another block type. This allows sharing the same properties between multiple block types.",
|
|
404
|
+
)
|
|
405
|
+
variant_of: str | None = Field(
|
|
406
|
+
None,
|
|
407
|
+
alias="variantOf",
|
|
408
|
+
description="Indicates whether this block type is a variant of another block type. The value corresponds to the block type it is a variant of.",
|
|
409
|
+
)
|
|
410
|
+
name: str = Field(description="The name of the block type.")
|
|
411
|
+
any_name: bool | None = Field(
|
|
412
|
+
None,
|
|
413
|
+
alias="anyName",
|
|
414
|
+
description="A block with `anyName` can have any name. This requires strict placement in parent blocks to correctly validate the block parameters.",
|
|
415
|
+
)
|
|
416
|
+
short_description: str | None = Field(
|
|
417
|
+
None,
|
|
418
|
+
alias="shortDescription",
|
|
419
|
+
description="A brief description of the block type, used for quick reference in completion items.",
|
|
420
|
+
)
|
|
421
|
+
description: str | None = Field(None, description="A brief description of the block type.")
|
|
422
|
+
block_id: BlockID | None = Field(None, alias="ID")
|
|
423
|
+
parents: list[str] | None = Field(None, description="A list of valid parent block types.")
|
|
424
|
+
needs_children: list[str] | None = Field(
|
|
425
|
+
None,
|
|
426
|
+
alias="needsChildren",
|
|
427
|
+
description="A list of required child block types.",
|
|
428
|
+
)
|
|
429
|
+
soft_override: bool | None = Field(
|
|
430
|
+
None,
|
|
431
|
+
alias="softOverride",
|
|
432
|
+
description="Indicates whether the block can be soft overridden in scripts.",
|
|
433
|
+
)
|
|
434
|
+
is_root: bool | None = Field(
|
|
435
|
+
None,
|
|
436
|
+
alias="isRoot",
|
|
437
|
+
description="Indicates whether the block is a root document.",
|
|
438
|
+
)
|
|
439
|
+
pattern: list[str] | None = Field(
|
|
440
|
+
None,
|
|
441
|
+
description="A list of regex patterns that can be used to match this root file. Required when 'isRoot' is true.",
|
|
442
|
+
)
|
|
443
|
+
no_comma: bool | None = Field(
|
|
444
|
+
None,
|
|
445
|
+
alias="noComma",
|
|
446
|
+
description="Indicates whether the block parameters should not be followed by a comma.",
|
|
447
|
+
)
|
|
448
|
+
deprecated: BlockDeprecation | None = None
|
|
449
|
+
parameters: list[ParameterDefinition] | None = Field(
|
|
450
|
+
None,
|
|
451
|
+
description="A list of parameters available for this block type.",
|
|
452
|
+
)
|
|
453
|
+
properties: dict[str, Any] | None = Field(
|
|
454
|
+
None,
|
|
455
|
+
description="Custom additional properties for the block type.",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
@model_validator(mode="after")
|
|
459
|
+
def _root_requires_pattern(self) -> Self:
|
|
460
|
+
if self.is_root is True and self.pattern is None:
|
|
461
|
+
raise ValueError("'pattern' is required when 'isRoot' is true")
|
|
462
|
+
return self
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class _BlocksJsonSchemaGenerator(GenerateJsonSchema):
|
|
466
|
+
"""Draft-07 output without null members, titles or `default: null` entries, matching the hand-written schema."""
|
|
467
|
+
|
|
468
|
+
schema_dialect = "http://json-schema.org/draft-07/schema#"
|
|
469
|
+
|
|
470
|
+
def field_title_should_be_set(self, schema: Any) -> bool:
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue:
|
|
474
|
+
json_schema = super().model_schema(schema)
|
|
475
|
+
if schema["cls"] is not BlockDefinition:
|
|
476
|
+
json_schema.pop("title", None)
|
|
477
|
+
return json_schema
|
|
478
|
+
|
|
479
|
+
def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue:
|
|
480
|
+
return self.generate_inner(schema["schema"])
|
|
481
|
+
|
|
482
|
+
def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
|
|
483
|
+
if schema.get("default") is None:
|
|
484
|
+
return self.generate_inner(schema["schema"])
|
|
485
|
+
return super().default_schema(schema)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def build_json_schema() -> dict[str, Any]:
|
|
489
|
+
return BlockDefinition.model_json_schema(by_alias=True, schema_generator=_BlocksJsonSchemaGenerator)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def main(argv: list[str]) -> int:
|
|
493
|
+
output = Path(argv[0]) if argv else DEFAULT_OUTPUT
|
|
494
|
+
output.write_text(json.dumps(build_json_schema(), indent=2) + "\n", encoding="utf-8")
|
|
495
|
+
print(f"Wrote {output}")
|
|
496
|
+
return 0
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
if __name__ == "__main__":
|
|
500
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
INPUT_FILE = "out/scriptsBlocks.json"
|
|
4
|
+
OUTPUT_FILE = "out/itemParameters.json"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_itemtype_needs(param_data: dict) -> list[str] | None:
|
|
9
|
+
# skip if no needs
|
|
10
|
+
if 'needs' not in param_data:
|
|
11
|
+
return None
|
|
12
|
+
# skip if no need for ItemType
|
|
13
|
+
for need in param_data['needs']:
|
|
14
|
+
if need.get('name', None) == 'ItemType':
|
|
15
|
+
return need.get('values', None)
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# load item ItemTypes available
|
|
20
|
+
with open(INPUT_FILE, 'r', encoding='utf-8') as f:
|
|
21
|
+
blocks = json.load(f)
|
|
22
|
+
|
|
23
|
+
item_block = blocks['item']
|
|
24
|
+
itemtypes = item_block['parameters']['itemtype']['values']
|
|
25
|
+
|
|
26
|
+
# initialize dictionary of ItemTypes
|
|
27
|
+
type_2_params = {}
|
|
28
|
+
for itemtype in itemtypes:
|
|
29
|
+
type_2_params[itemtype] = []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# for each parameter, check if it has a need for ItemType
|
|
33
|
+
# if so, it means it specifically is only valid those ItemTypes
|
|
34
|
+
for param_key, param_data in item_block['parameters'].items():
|
|
35
|
+
itemtypes = get_itemtype_needs(param_data)
|
|
36
|
+
if itemtypes is not None:
|
|
37
|
+
for itemtype in itemtypes:
|
|
38
|
+
type_2_params[itemtype].append(param_data['name'])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
|
|
42
|
+
json.dump(type_2_params, f, indent=4)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validate YAML block files against the blocks.json schema.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python validateBlocks.py # Validate all YAML files in data/blocks/
|
|
7
|
+
python validateBlocks.py <file_path> # Validate a specific file
|
|
8
|
+
python validateBlocks.py <directory_path> # Validate all YAML files in a directory
|
|
9
|
+
python validateBlocks.py --help # Show this help message
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json, yaml, sys, os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from jsonschema import Draft7Validator
|
|
15
|
+
from typing import List, Tuple
|
|
16
|
+
|
|
17
|
+
# Default paths
|
|
18
|
+
DEFAULT_BLOCKS_DIR = "data/blocks"
|
|
19
|
+
DEFAULT_SCHEMA_PATH = "schemas/blocks.json"
|
|
20
|
+
|
|
21
|
+
def load_schema(schema_path: str) -> dict:
|
|
22
|
+
"""Load the JSON schema from file."""
|
|
23
|
+
with open(schema_path, 'r') as f:
|
|
24
|
+
return json.load(f)
|
|
25
|
+
|
|
26
|
+
def load_yaml_file(file_path: str) -> dict:
|
|
27
|
+
"""Load a YAML file and return its contents."""
|
|
28
|
+
with open(file_path, 'r') as f:
|
|
29
|
+
return yaml.safe_load(f)
|
|
30
|
+
|
|
31
|
+
def validate_file(file_path: str, schema: dict) -> Tuple[bool, List[str]]:
|
|
32
|
+
"""
|
|
33
|
+
Validate a single YAML file against the schema.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (is_valid, error_messages)
|
|
37
|
+
"""
|
|
38
|
+
errors = []
|
|
39
|
+
try:
|
|
40
|
+
data = load_yaml_file(file_path)
|
|
41
|
+
|
|
42
|
+
# Handle empty files
|
|
43
|
+
if data is None:
|
|
44
|
+
return True, []
|
|
45
|
+
|
|
46
|
+
# Validate against schema
|
|
47
|
+
validator = Draft7Validator(schema)
|
|
48
|
+
validation_errors = list(validator.iter_errors(data))
|
|
49
|
+
|
|
50
|
+
if validation_errors:
|
|
51
|
+
for error in validation_errors:
|
|
52
|
+
path = ".".join(str(p) for p in error.absolute_path) or "root"
|
|
53
|
+
errors.append(f" {path}: {error.message}")
|
|
54
|
+
return False, errors
|
|
55
|
+
|
|
56
|
+
return True, []
|
|
57
|
+
|
|
58
|
+
except yaml.YAMLError as e:
|
|
59
|
+
return False, [f" YAML Parse Error: {str(e)}"]
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return False, [f" Error: {str(e)}"]
|
|
62
|
+
|
|
63
|
+
def get_yaml_files(path: str) -> List[str]:
|
|
64
|
+
"""Get all YAML files from a path (file or directory)."""
|
|
65
|
+
path_obj = Path(path)
|
|
66
|
+
|
|
67
|
+
if path_obj.is_file():
|
|
68
|
+
if path_obj.suffix.lower() in ['.yaml', '.yml']:
|
|
69
|
+
return [str(path_obj)]
|
|
70
|
+
return []
|
|
71
|
+
elif path_obj.is_dir():
|
|
72
|
+
return sorted([str(f) for f in path_obj.glob("*.yaml")] +
|
|
73
|
+
[str(f) for f in path_obj.glob("*.yml")])
|
|
74
|
+
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
"""Main validation function."""
|
|
79
|
+
# Parse arguments
|
|
80
|
+
if len(sys.argv) > 1:
|
|
81
|
+
if sys.argv[1] in ['--help', '-h', 'help']:
|
|
82
|
+
print(__doc__)
|
|
83
|
+
return 0
|
|
84
|
+
target_path = sys.argv[1]
|
|
85
|
+
else:
|
|
86
|
+
target_path = DEFAULT_BLOCKS_DIR
|
|
87
|
+
|
|
88
|
+
# Check if project root is correct
|
|
89
|
+
if not os.path.exists(DEFAULT_SCHEMA_PATH):
|
|
90
|
+
print(f"Error: Schema not found at {DEFAULT_SCHEMA_PATH}")
|
|
91
|
+
print("Please run this script from the project root directory.")
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
# Load schema
|
|
95
|
+
try:
|
|
96
|
+
schema = load_schema(DEFAULT_SCHEMA_PATH)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f"Error loading schema: {e}")
|
|
99
|
+
return 1
|
|
100
|
+
|
|
101
|
+
# Get files to validate
|
|
102
|
+
if not os.path.exists(target_path):
|
|
103
|
+
print(f"Error: Path not found: {target_path}")
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
yaml_files = get_yaml_files(target_path)
|
|
107
|
+
|
|
108
|
+
if not yaml_files:
|
|
109
|
+
print(f"No YAML files found in {target_path}")
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
# Validate files
|
|
113
|
+
print(f"Validating {len(yaml_files)} file(s)...\n")
|
|
114
|
+
|
|
115
|
+
valid_count = 0
|
|
116
|
+
invalid_count = 0
|
|
117
|
+
|
|
118
|
+
for file_path in yaml_files:
|
|
119
|
+
is_valid, errors = validate_file(file_path, schema)
|
|
120
|
+
|
|
121
|
+
if is_valid:
|
|
122
|
+
print(f"✓ {file_path}")
|
|
123
|
+
valid_count += 1
|
|
124
|
+
else:
|
|
125
|
+
print(f"✗ {file_path}")
|
|
126
|
+
for error in errors:
|
|
127
|
+
print(error)
|
|
128
|
+
print()
|
|
129
|
+
invalid_count += 1
|
|
130
|
+
|
|
131
|
+
# Summary
|
|
132
|
+
print("\n" + "="*60)
|
|
133
|
+
print(f"Results: {valid_count} valid, {invalid_count} invalid")
|
|
134
|
+
print("="*60)
|
|
135
|
+
|
|
136
|
+
return 0 if invalid_count == 0 else 1
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
sys.exit(main())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pz-scripts-data
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Dataset for the Scripts of Project Zomboid.
|
|
5
|
+
Author: SimKDT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Topic :: Database
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.12.3
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pyyaml
|
|
16
|
+
Requires-Dist: jsonschema
|
|
17
|
+
Requires-Dist: pydantic>=2.11
|
|
18
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pz_scripts_data/blocks.py
|
|
5
|
+
src/pz_scripts_data/formatBlocks.py
|
|
6
|
+
src/pz_scripts_data/py.typed
|
|
7
|
+
src/pz_scripts_data/release.py
|
|
8
|
+
src/pz_scripts_data/schema.py
|
|
9
|
+
src/pz_scripts_data/sortItemParameters.py
|
|
10
|
+
src/pz_scripts_data/validateBlocks.py
|
|
11
|
+
src/pz_scripts_data.egg-info/PKG-INFO
|
|
12
|
+
src/pz_scripts_data.egg-info/SOURCES.txt
|
|
13
|
+
src/pz_scripts_data.egg-info/dependency_links.txt
|
|
14
|
+
src/pz_scripts_data.egg-info/entry_points.txt
|
|
15
|
+
src/pz_scripts_data.egg-info/requires.txt
|
|
16
|
+
src/pz_scripts_data.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pz_scripts_data
|