pyrecli 0.3.5__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.

Potentially problematic release.


This version of pyrecli might be problematic. Click here for more details.

pyrecli-0.3.5/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Amp63
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pyrecli-0.3.5/PKG-INFO ADDED
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrecli
3
+ Version: 0.3.5
4
+ Summary: Command line utilities for DiamondFire templates
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: diamondfire,minecraft,template,cli,tools
8
+ Author: Amp
9
+ Requires-Python: >=3.10
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Provides-Extra: dev
17
+ Requires-Dist: dfpyre (>=0.10.6)
18
+ Requires-Dist: pytest (>=9.0.2) ; extra == "dev"
19
+ Requires-Dist: rapidnbt (>=1.3.5)
20
+ Requires-Dist: twine (>=6.2.0) ; extra == "dev"
21
+ Project-URL: Repository, https://github.com/Amp63/pyrecli
22
+ Description-Content-Type: text/markdown
23
+
24
+ # pyrecli
25
+
26
+ Command line utilities for DiamondFire templates
27
+
28
+ ## Installation
29
+
30
+ Run the following command in a terminal:
31
+
32
+ ```sh
33
+ pip install pyrecli
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ - `scan`: Scan all templates on the plot and dump them to a text file (requires [CodeClient](github.com/DFOnline/CodeClient))
39
+ - `send`: Send template items to DiamondFire (requires [CodeClient](github.com/DFOnline/CodeClient))
40
+ - `rename`: Rename all occurences of a variable (including text codes)
41
+ - `script`: Generate python scripts from template data
42
+ - `grabinv`: Save all templates in your Minecraft inventory to a file (requires [CodeClient](github.com/DFOnline/CodeClient))
43
+ - `docs`: Generate markdown documentation from template data
44
+ - `slice`: Slice a template into multiple smaller templates
45
+ - `cctoken`: Get a reusable CodeClient authentication token
46
+
47
+
48
+ ## What is this useful for?
49
+
50
+ - Backing up a plot
51
+ - Getting an accurate text representation of DF code
52
+ - Open sourcing
53
+ - Version control
54
+ - Large scale refactoring
55
+
56
+
57
+ ## Example Command Usages
58
+
59
+ ### Scan
60
+
61
+ **[Requires CodeClient]**
62
+
63
+ Grabs all of the templates on your current plot and saves them to a file.
64
+ You will need to run `/auth` in-game to authorize this action.
65
+
66
+ Example:
67
+ ```sh
68
+ # Dumps all template data into templates.dfts
69
+ pyrecli scan templates.dfts
70
+ ```
71
+
72
+ ### Send
73
+
74
+ **[Requires CodeClient]**
75
+
76
+ Sends all templates in a file back to your inventory.
77
+
78
+ Example:
79
+ ```sh
80
+ pyrecli send templates.dfts
81
+ ```
82
+
83
+ ### Rename
84
+
85
+ Renames all occurences of a variable in a list of templates.
86
+ You can run this command on a single template, or on an entire plot if a variable is used in many places.
87
+
88
+ This command still requires thorough testing, so make sure you have a backup of your plot before using this command on a large scale.
89
+
90
+ Example:
91
+ ```sh
92
+ # Changes all variables named `foo` to `bar`, then saves the new templates to 'renamed.dfts'.
93
+ pyrecli rename templates.dfts renamed.dfts foo bar
94
+
95
+ # You can also target a specific scope.
96
+ # This changes all occurences of the game variable `plotData` to `gameData`.
97
+ pyrecli rename templates.dfts renamed.dfts plotData gameData -s game
98
+ ```
99
+
100
+ ### Script
101
+
102
+ Generates Python scripts from template data.
103
+
104
+ Example:
105
+ ```sh
106
+ # Convert templates into individual scripts and store them in directory `plot_templates`
107
+ pyrecli script templates.dfts plot_templates
108
+
109
+ # Convert templates into scripts and put them into a single file `plot_templates.py`
110
+ pyrecli script templates.dfts plot_templates.py --onefile
111
+ ```
112
+
113
+
114
+ ### Grabinv
115
+
116
+ **[Requires CodeClient]**
117
+
118
+ Scans your inventory for templates and saves them to a file.
119
+
120
+ Example:
121
+ ```sh
122
+ # Save inventory templates to `templates.dfts`
123
+ pyrecli grabinv templates.dfts
124
+ ```
125
+
126
+
127
+ ### Docs
128
+
129
+ Generates a Markdown documentation file for a list of templates.
130
+
131
+ Example:
132
+ ```sh
133
+ # Generate documentation and save it to `plot_docs.md`
134
+ pyrecli docs templates.dfts plot_docs.md "My Plot Docs"
135
+ ```
136
+
137
+
138
+ ### Slice
139
+
140
+ Slices a single template into multiple smaller templates.
141
+ This is useful for resizing templates to fit on a smaller plot.
142
+
143
+ If multiple templates are passed, only the first one will be used.
144
+
145
+ **NOTE: This feature is not fully implemented yet. Any templates with Control::Return blocks may not work properly if sliced.**
146
+
147
+ Example:
148
+ ```sh
149
+ # Slices the first template in `templates.dfts` with a target length of 50 and stores them in `sliced_templates.dfts`
150
+ pyrecli slice templates.dfts sliced_templates.dfts 50
151
+ ```
152
+
153
+
154
+ ### CCToken
155
+
156
+ Returns a CodeClient authentication token that can be used in commands that require CodeClient authorization.
157
+ This is useful for reducing the amount of times you need to run `/auth`.
158
+
159
+ Example:
160
+ ```sh
161
+ # Get a token with the read_plot and inventory scopes
162
+ pyrecli cctoken mytoken.txt "read_plot inventory"
163
+ ```
164
+
165
+
166
+ ### Command Chaining
167
+
168
+ You can combine the pipe operator (`|`) with hyphen (`-`) file paths to chain multiple commands together.
169
+
170
+ Example:
171
+ ```sh
172
+ # Scans the plot, renames a variable, then sends renamed templates back to DiamondFire
173
+ pyrecli scan - | pyrecli rename - foo bar | pyrecli send -
174
+
175
+ ```
@@ -0,0 +1,152 @@
1
+ # pyrecli
2
+
3
+ Command line utilities for DiamondFire templates
4
+
5
+ ## Installation
6
+
7
+ Run the following command in a terminal:
8
+
9
+ ```sh
10
+ pip install pyrecli
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ - `scan`: Scan all templates on the plot and dump them to a text file (requires [CodeClient](github.com/DFOnline/CodeClient))
16
+ - `send`: Send template items to DiamondFire (requires [CodeClient](github.com/DFOnline/CodeClient))
17
+ - `rename`: Rename all occurences of a variable (including text codes)
18
+ - `script`: Generate python scripts from template data
19
+ - `grabinv`: Save all templates in your Minecraft inventory to a file (requires [CodeClient](github.com/DFOnline/CodeClient))
20
+ - `docs`: Generate markdown documentation from template data
21
+ - `slice`: Slice a template into multiple smaller templates
22
+ - `cctoken`: Get a reusable CodeClient authentication token
23
+
24
+
25
+ ## What is this useful for?
26
+
27
+ - Backing up a plot
28
+ - Getting an accurate text representation of DF code
29
+ - Open sourcing
30
+ - Version control
31
+ - Large scale refactoring
32
+
33
+
34
+ ## Example Command Usages
35
+
36
+ ### Scan
37
+
38
+ **[Requires CodeClient]**
39
+
40
+ Grabs all of the templates on your current plot and saves them to a file.
41
+ You will need to run `/auth` in-game to authorize this action.
42
+
43
+ Example:
44
+ ```sh
45
+ # Dumps all template data into templates.dfts
46
+ pyrecli scan templates.dfts
47
+ ```
48
+
49
+ ### Send
50
+
51
+ **[Requires CodeClient]**
52
+
53
+ Sends all templates in a file back to your inventory.
54
+
55
+ Example:
56
+ ```sh
57
+ pyrecli send templates.dfts
58
+ ```
59
+
60
+ ### Rename
61
+
62
+ Renames all occurences of a variable in a list of templates.
63
+ You can run this command on a single template, or on an entire plot if a variable is used in many places.
64
+
65
+ This command still requires thorough testing, so make sure you have a backup of your plot before using this command on a large scale.
66
+
67
+ Example:
68
+ ```sh
69
+ # Changes all variables named `foo` to `bar`, then saves the new templates to 'renamed.dfts'.
70
+ pyrecli rename templates.dfts renamed.dfts foo bar
71
+
72
+ # You can also target a specific scope.
73
+ # This changes all occurences of the game variable `plotData` to `gameData`.
74
+ pyrecli rename templates.dfts renamed.dfts plotData gameData -s game
75
+ ```
76
+
77
+ ### Script
78
+
79
+ Generates Python scripts from template data.
80
+
81
+ Example:
82
+ ```sh
83
+ # Convert templates into individual scripts and store them in directory `plot_templates`
84
+ pyrecli script templates.dfts plot_templates
85
+
86
+ # Convert templates into scripts and put them into a single file `plot_templates.py`
87
+ pyrecli script templates.dfts plot_templates.py --onefile
88
+ ```
89
+
90
+
91
+ ### Grabinv
92
+
93
+ **[Requires CodeClient]**
94
+
95
+ Scans your inventory for templates and saves them to a file.
96
+
97
+ Example:
98
+ ```sh
99
+ # Save inventory templates to `templates.dfts`
100
+ pyrecli grabinv templates.dfts
101
+ ```
102
+
103
+
104
+ ### Docs
105
+
106
+ Generates a Markdown documentation file for a list of templates.
107
+
108
+ Example:
109
+ ```sh
110
+ # Generate documentation and save it to `plot_docs.md`
111
+ pyrecli docs templates.dfts plot_docs.md "My Plot Docs"
112
+ ```
113
+
114
+
115
+ ### Slice
116
+
117
+ Slices a single template into multiple smaller templates.
118
+ This is useful for resizing templates to fit on a smaller plot.
119
+
120
+ If multiple templates are passed, only the first one will be used.
121
+
122
+ **NOTE: This feature is not fully implemented yet. Any templates with Control::Return blocks may not work properly if sliced.**
123
+
124
+ Example:
125
+ ```sh
126
+ # Slices the first template in `templates.dfts` with a target length of 50 and stores them in `sliced_templates.dfts`
127
+ pyrecli slice templates.dfts sliced_templates.dfts 50
128
+ ```
129
+
130
+
131
+ ### CCToken
132
+
133
+ Returns a CodeClient authentication token that can be used in commands that require CodeClient authorization.
134
+ This is useful for reducing the amount of times you need to run `/auth`.
135
+
136
+ Example:
137
+ ```sh
138
+ # Get a token with the read_plot and inventory scopes
139
+ pyrecli cctoken mytoken.txt "read_plot inventory"
140
+ ```
141
+
142
+
143
+ ### Command Chaining
144
+
145
+ You can combine the pipe operator (`|`) with hyphen (`-`) file paths to chain multiple commands together.
146
+
147
+ Example:
148
+ ```sh
149
+ # Scans the plot, renames a variable, then sends renamed templates back to DiamondFire
150
+ pyrecli scan - | pyrecli rename - foo bar | pyrecli send -
151
+
152
+ ```
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "pyrecli"
3
+ version = "0.3.5"
4
+ description = "Command line utilities for DiamondFire templates"
5
+ authors = [
6
+ {name = "Amp"}
7
+ ]
8
+ license = "MIT"
9
+ readme = "README.md"
10
+ keywords = ["diamondfire", "minecraft", "template", "cli", "tools"]
11
+
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+ "dfpyre>=0.10.6",
15
+ "rapidnbt>=1.3.5"
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=9.0.2",
21
+ "twine>=6.2.0"
22
+ ]
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/Amp63/pyrecli"
26
+
27
+
28
+ [tool.poetry.scripts]
29
+ pyrecli = "pyrecli.pyrecli:main"
30
+
31
+
32
+ [build-system]
33
+ requires = ["poetry-core"]
34
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,10 @@
1
+ from pyrecli.util import connect_to_codeclient, write_output_file
2
+
3
+
4
+ def cctoken_command(output_path: str, scopes: str):
5
+ ws = connect_to_codeclient(scopes)
6
+
7
+ ws.send('token')
8
+ token = ws.recv().replace('token ', '')
9
+
10
+ write_output_file(output_path, token)
@@ -0,0 +1,115 @@
1
+ from typing import Literal, TypedDict
2
+ from dfpyre import DFTemplate, Item, Parameter
3
+ from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string
4
+
5
+
6
+ STARTER_BLOCK_LOOKUP = {
7
+ 'func': 'Function',
8
+ 'process': 'Process'
9
+ }
10
+
11
+
12
+ def escape_md(s: str) -> str:
13
+ MD_CHARS = r'*`$#!&^~'
14
+ for char in MD_CHARS:
15
+ s = s.replace(char, rf'\{char}')
16
+ return s
17
+
18
+
19
+ class TemplateDocData(TypedDict):
20
+ template_type: Literal['Function', 'Process']
21
+ function_name: str
22
+ doc_lines: list[str]
23
+
24
+
25
+ def docs_command(input_path: str, output_path: str, title: str, include_hidden: bool, omit_toc: bool):
26
+ templates_string = read_input_file(input_path)
27
+ templates = parse_templates_from_string(templates_string)
28
+
29
+ def get_function_name(template: DFTemplate) -> str:
30
+ first_block = template.codeblocks[0]
31
+ function_name = first_block.data.get('data')
32
+ return escape_md(function_name)
33
+
34
+ block_type_order = list(STARTER_BLOCK_LOOKUP.keys())
35
+ templates = [t for t in templates if t.codeblocks[0].action_name == 'dynamic']
36
+ templates.sort(key=get_function_name)
37
+ templates.sort(key=lambda t: block_type_order.index(t.codeblocks[0].type))
38
+
39
+ output_lines: list[str] = [
40
+ f'# {title}',
41
+ ''
42
+ ]
43
+
44
+ template_docs: list[TemplateDocData] = []
45
+ for template in templates:
46
+ first_block = template.codeblocks[0]
47
+
48
+ # Skip if hidden
49
+ if first_block.tags.get('Is Hidden') == 'True' and not include_hidden:
50
+ continue
51
+
52
+ # Add function / process name
53
+ template_type = STARTER_BLOCK_LOOKUP[first_block.type]
54
+ template_name = get_function_name(template)
55
+ template_doc_lines: list[str] = []
56
+
57
+ template_doc_lines.append(f'## {template_type}: {template_name}')
58
+
59
+ # Parse description
60
+ if first_block.args:
61
+ first_arg = first_block.args[0]
62
+ if isinstance(first_arg, Item):
63
+ try:
64
+ lore_text = [escape_md(l.to_string()) for l in first_arg.get_lore()]
65
+ if lore_text:
66
+ template_doc_lines.extend(lore_text)
67
+ except:
68
+ # There are so many things that can go wrong here due to various legacy
69
+ # item formats and weird MC string edge cases, so we can just skip
70
+ # if there's a problem.
71
+ pass
72
+
73
+ # Parse parameters
74
+ parameter_lines: list[str] = []
75
+ for arg in first_block.args:
76
+ if isinstance(arg, Parameter):
77
+ optional_text = "*" if arg.optional else ""
78
+ default_value_text = f'= `{escape_md(arg.default_value.__repr__())}`' if arg.default_value else ''
79
+ parameter_lines.append(f'- *`{escape_md(arg.name)}{optional_text}`*: `{arg.param_type.get_string_value()}` {default_value_text}')
80
+ if arg.description:
81
+ parameter_lines.append(f' - {escape_md(arg.description)}')
82
+ if arg.note:
83
+ parameter_lines.append(f' - {escape_md(arg.note)}')
84
+ if parameter_lines:
85
+ template_doc_lines.append('')
86
+ template_doc_lines.append('### Parameters:')
87
+ template_doc_lines.extend(parameter_lines)
88
+
89
+ doc_data = TemplateDocData(template_type=template_type, function_name=template_name, doc_lines=template_doc_lines)
90
+ template_docs.append(doc_data)
91
+
92
+
93
+ # Add table of contents
94
+ def add_toc_group(doc_data_list: list[TemplateDocData], group_title: str):
95
+ if doc_data_list:
96
+ output_lines.append(f'### {group_title}')
97
+ for doc_data in doc_data_list:
98
+ link = f'#{doc_data["template_type"]}-{doc_data["function_name"]}'.lower().replace(' ', '-')
99
+ output_lines.append(f'- [{doc_data["function_name"]}]({link})')
100
+
101
+ if not omit_toc:
102
+ output_lines.append('## Contents')
103
+ function_templates = [d for d in template_docs if d['template_type'] == 'Function']
104
+ add_toc_group(function_templates, 'Functions')
105
+ output_lines.append('')
106
+ process_templates = [d for d in template_docs if d['template_type'] == 'Process']
107
+ add_toc_group(process_templates, 'Processes')
108
+ output_lines.append('\n')
109
+
110
+ # Add template docs to output lines
111
+ for doc_data in template_docs:
112
+ output_lines.extend(doc_data['doc_lines'])
113
+ output_lines.append('')
114
+
115
+ write_output_file(output_path, '\n'.join(output_lines))
@@ -0,0 +1,44 @@
1
+ import json
2
+ from rapidnbt import nbtio, CompoundTagVariant
3
+ from pyrecli.util import write_output_file, connect_to_codeclient, print_status
4
+
5
+
6
+ def grabinv_command(output_path: str, token: str|None=None):
7
+ ws = connect_to_codeclient('inventory', token)
8
+
9
+ ws.send('inv')
10
+ inventory = ws.recv()
11
+ ws.close()
12
+
13
+ inventory = f'{{inventory:{inventory}}}'
14
+ inventory_nbt = nbtio.loads_snbt(inventory)
15
+
16
+ template_codes: list[str] = []
17
+ for tag in inventory_nbt['inventory']:
18
+ tag: CompoundTagVariant
19
+ components = tag['components']
20
+ if components.is_null():
21
+ continue
22
+
23
+ custom_data = components['minecraft:custom_data']
24
+ if custom_data.is_null():
25
+ continue
26
+
27
+ pbv_tag = custom_data['PublicBukkitValues']
28
+ if pbv_tag.is_null():
29
+ continue
30
+
31
+ code_template_data = pbv_tag['hypercube:codetemplatedata']
32
+ if code_template_data.is_null():
33
+ continue
34
+
35
+ code_template_json = json.loads(code_template_data.get_string())
36
+
37
+ template_code = code_template_json.get('code')
38
+ if template_code:
39
+ template_codes.append(template_code)
40
+
41
+ if not template_codes:
42
+ print_status('Could not find any templates in the inventory.')
43
+
44
+ write_output_file(output_path, '\n'.join(template_codes))
@@ -0,0 +1,53 @@
1
+ from typing import Literal
2
+ import re
3
+ from dfpyre import Variable, Number, String, Text, Parameter
4
+ from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string
5
+
6
+
7
+ TEXT_CODE_PATTERNS = [
8
+ re.compile(r"%var\(([a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+)\)"),
9
+ re.compile(r"%index\(([a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+),\d+\)"),
10
+ re.compile(r"%entry\(([a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+),[a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+\)")
11
+ ]
12
+
13
+
14
+ def rename_var_in_text_code(s: str, var_to_rename: str, new_var_name: str):
15
+ for pattern in TEXT_CODE_PATTERNS:
16
+ match = pattern.search(s)
17
+ if match and match.group(1) == var_to_rename:
18
+ s = s.replace(match.group(1), new_var_name)
19
+ return s
20
+
21
+
22
+ def rename_command(input_path: str, output_path: str,
23
+ var_to_rename: str, new_var_name: str,
24
+ var_to_rename_scope: Literal['game', 'saved', 'local', 'line']|None):
25
+
26
+ templates_string = read_input_file(input_path)
27
+ templates = parse_templates_from_string(templates_string)
28
+
29
+ for template in templates:
30
+ for codeblock in template.codeblocks:
31
+ for argument in codeblock.args:
32
+ # Try to rename variable
33
+ if isinstance(argument, Variable):
34
+ if argument.name == var_to_rename and (var_to_rename_scope is None or argument.scope == var_to_rename_scope):
35
+ argument.name = new_var_name
36
+ argument.name = rename_var_in_text_code(argument.name, var_to_rename, new_var_name)
37
+
38
+ # Try to rename parameter
39
+ elif isinstance(argument, Parameter) and var_to_rename_scope == 'line':
40
+ if argument.name == var_to_rename:
41
+ argument.name = new_var_name
42
+
43
+ # Check for occurrences of the variable in text codes
44
+ elif isinstance(argument, (Number, String, Text)) and isinstance(argument.value, str):
45
+ argument.value = rename_var_in_text_code(argument.value, var_to_rename, new_var_name)
46
+
47
+ # Check for text codes in function calls
48
+ if codeblock.type in {'call_func', 'start_process'}:
49
+ new_data = rename_var_in_text_code(codeblock.data.get('data'), var_to_rename, new_var_name)
50
+ codeblock.data['data'] = new_data
51
+
52
+ new_file_content = '\n'.join(t.build() for t in templates)
53
+ write_output_file(output_path, new_file_content)
@@ -0,0 +1,14 @@
1
+ from pyrecli.util import write_output_file, connect_to_codeclient, print_status
2
+
3
+
4
+ def scan_command(output_path: str, token: str|None=None):
5
+ ws = connect_to_codeclient('read_plot', token)
6
+
7
+ print_status('Scanning plot...')
8
+ ws.send('scan')
9
+
10
+ scan_results = ws.recv()
11
+ print_status('Done.')
12
+ ws.close()
13
+
14
+ write_output_file(output_path, scan_results)
@@ -0,0 +1,36 @@
1
+ import os
2
+ from dfpyre import DFTemplate
3
+ from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string
4
+
5
+
6
+ def write_to_directory(dir_name: str, templates: list[DFTemplate], flags: dict[str, int|bool]):
7
+ if not os.path.isdir(dir_name):
8
+ os.mkdir(dir_name)
9
+
10
+ for template in templates:
11
+ script_path = f'{dir_name}/{template.get_template_name()}.py'
12
+ script_string = template.generate_script(**flags)
13
+ with open(script_path, 'w') as f:
14
+ f.write(script_string)
15
+
16
+
17
+ def write_to_single_file(file_path: str, templates: list[DFTemplate], flags: dict[str, int|bool]):
18
+ file_content = []
19
+ for i, template in enumerate(templates):
20
+ if i == 0:
21
+ template_script = template.generate_script(include_import=True, assign_variable=True, **flags)
22
+ else:
23
+ template_script = template.generate_script(include_import=False, assign_variable=True, **flags)
24
+ file_content.append(template_script)
25
+
26
+ write_output_file(file_path, '\n\n'.join(file_content))
27
+
28
+
29
+ def script_command(input_path: str, output_path: str, one_file: bool, flags: dict[str, int|bool]):
30
+ templates_string = read_input_file(input_path)
31
+ templates = parse_templates_from_string(templates_string)
32
+
33
+ if one_file or output_path == '-':
34
+ return write_to_single_file(output_path, templates, flags)
35
+
36
+ return write_to_directory(output_path, templates, flags)
@@ -0,0 +1,16 @@
1
+ from pyrecli.util import connect_to_codeclient, read_input_file, parse_templates_from_string, print_status
2
+
3
+
4
+ def send_command(input_path: str):
5
+ templates_string = read_input_file(input_path)
6
+ templates = parse_templates_from_string(templates_string)
7
+
8
+ ws = connect_to_codeclient()
9
+
10
+ for template in templates:
11
+ item = template.generate_template_item()
12
+ ws.send(f'give {item.get_snbt()}')
13
+
14
+ ws.close()
15
+
16
+ print_status(f'Sent {len(templates)} template{"s" if len(templates) != 1 else ''} successfully.')
@@ -0,0 +1,15 @@
1
+ from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string, NoTemplatesError
2
+
3
+
4
+ def slice_command(input_path: str, output_path: str, target_length: int):
5
+ templates_string = read_input_file(input_path)
6
+ templates = parse_templates_from_string(templates_string)
7
+
8
+ if not templates:
9
+ raise NoTemplatesError(f'Could not find any templates in {input_path}')
10
+
11
+ first_template = templates[0]
12
+ sliced_templates = first_template.slice(target_length)
13
+ built_templates = [t.build() for t in sliced_templates]
14
+
15
+ write_output_file(output_path, '\n'.join(built_templates))
@@ -0,0 +1,132 @@
1
+ import sys
2
+ import argparse
3
+ import importlib.metadata
4
+
5
+ from pyrecli.command.scan import scan_command
6
+ from pyrecli.command.send import send_command
7
+ from pyrecli.command.script import script_command
8
+ from pyrecli.command.rename import rename_command
9
+ from pyrecli.command.grabinv import grabinv_command
10
+ from pyrecli.command.docs import docs_command
11
+ from pyrecli.command.slice import slice_command
12
+ from pyrecli.command.cctoken import cctoken_command
13
+ from pyrecli.util import print_status
14
+
15
+
16
+ def rename_target_scope(value):
17
+ SCOPES = {'game', 'saved', 'local', 'line'}
18
+ if value not in SCOPES:
19
+ raise argparse.ArgumentTypeError(f'Expected one of {SCOPES} for rename target scope')
20
+ return value
21
+
22
+
23
+ def slice_target_length(value):
24
+ MINIMUM_LENGTH = 5
25
+ ivalue = int(value)
26
+ if ivalue < MINIMUM_LENGTH:
27
+ raise argparse.ArgumentTypeError(f'Target length must be at least {MINIMUM_LENGTH} codeblocks')
28
+ return ivalue
29
+
30
+
31
+ def main() -> int:
32
+ parser = argparse.ArgumentParser(prog='pyrecli', description='Command line utilities for DiamondFire templates')
33
+ parser.add_argument('--version', '-v', action='version', version=f'pyrecli {importlib.metadata.version('pyrecli')}')
34
+ subparsers = parser.add_subparsers(dest='command', help='Available commands:', required=True, metavar='<command>')
35
+
36
+ parser_scan = subparsers.add_parser('scan', help='Scan the current plot templates with CodeClient')
37
+ parser_scan.add_argument('output_path', help='The file to output template data to', type=str)
38
+ parser_scan.add_argument('--token', '-t', help='The CodeClient authentication token to use', type=str, default=None)
39
+
40
+ parser_send = subparsers.add_parser('send', help='Send templates to DiamondFire with CodeClient')
41
+ parser_send.add_argument('input_path', help='The file containing template data', type=str)
42
+
43
+ parser_script = subparsers.add_parser('script', help='Create python scripts from template data')
44
+ parser_script.add_argument('input_path', help='The file containing template data', type=str)
45
+ parser_script.add_argument('output_path', help='The file or directory to output to', type=str)
46
+ parser_script.add_argument('--onefile', help='Output template data as a single script', action='store_true')
47
+ parser_script.add_argument('--indent_size', '-i', help='The multiple of spaces to add when indenting lines', type=int, default=4)
48
+ parser_script.add_argument('--literal_shorthand', '-ls', help='Output Text and Number items as strings and ints respectively', action='store_false')
49
+ parser_script.add_argument('--var_shorthand', '-vs', help='Write all variables using variable shorthand', action='store_true')
50
+ parser_script.add_argument('--preserve_slots', '-s', help='Save the positions of items within chests', action='store_true')
51
+ parser_script.add_argument('--build_and_send', '-b', help='Add `.build_and_send()` to the end of the generated template(s)', action='store_true')
52
+
53
+ parser_rename = subparsers.add_parser('rename', help='Rename all occurrences of a variable')
54
+ parser_rename.add_argument('input_path', help='The file containing template data', type=str)
55
+ parser_rename.add_argument('output_path', help='The file to output to', type=str)
56
+ parser_rename.add_argument('var_to_rename', help='The variable to rename', type=str)
57
+ parser_rename.add_argument('new_var_name', help='The new name for the variable', type=str)
58
+ parser_rename.add_argument('--var_scope', '-s', help='The scope to match', type=rename_target_scope, default=None)
59
+
60
+ parser_grabinv = subparsers.add_parser('grabinv', help='Save all templates in the inventory to a file with CodeClient')
61
+ parser_grabinv.add_argument('output_path', help='The file to output template data to', type=str)
62
+ parser_grabinv.add_argument('--token', '-t', help='The CodeClient authentication token to use', type=str, default=None)
63
+
64
+ parser_docs = subparsers.add_parser('docs', help='Generate markdown documentation from template data')
65
+ parser_docs.add_argument('input_path', help='The file containing template data', type=str)
66
+ parser_docs.add_argument('output_path', help='The file to output to', type=str)
67
+ parser_docs.add_argument('--title', '-t', help='The title for the docs', type=str, default='Template Docs')
68
+ parser_docs.add_argument('--include_hidden', '-ih', help='Include hidden functions and processes', action='store_true')
69
+ parser_docs.add_argument('--notoc', help='Omit the table of contents', action='store_true')
70
+
71
+ parser_slice = subparsers.add_parser('slice', help='Slice a template into multiple smaller templates')
72
+ parser_slice.add_argument('input_path', help='The file containing template data', type=str)
73
+ parser_slice.add_argument('output_path', help='The file to output template data to', type=str)
74
+ parser_slice.add_argument('target_length', help='The maximum length of each sliced template', type=slice_target_length)
75
+
76
+ parser_cctoken = subparsers.add_parser('cctoken', help='Request a CodeClient token with the specified scopes')
77
+ parser_cctoken.add_argument('output_path', help='The file to output the token to', type=str)
78
+ parser_cctoken.add_argument('scopes', help='The scopes to request', type=str)
79
+
80
+
81
+ parsed_args = parser.parse_args()
82
+
83
+ try:
84
+ match parsed_args.command:
85
+ case 'scan':
86
+ scan_command(parsed_args.output_path, parsed_args.token)
87
+
88
+ case 'send':
89
+ send_command(parsed_args.input_path)
90
+
91
+ case 'script':
92
+ scriptgen_flags = {
93
+ 'indent_size': parsed_args.indent_size,
94
+ 'literal_shorthand': parsed_args.literal_shorthand,
95
+ 'var_shorthand': parsed_args.var_shorthand,
96
+ 'preserve_slots': parsed_args.preserve_slots,
97
+ 'build_and_send': parsed_args.build_and_send
98
+ }
99
+ script_command(parsed_args.input_path, parsed_args.output_path, parsed_args.onefile, scriptgen_flags)
100
+
101
+ case 'rename':
102
+ rename_command(
103
+ parsed_args.input_path, parsed_args.output_path,
104
+ parsed_args.var_to_rename, parsed_args.new_var_name, parsed_args.var_scope
105
+ )
106
+
107
+ case 'grabinv':
108
+ grabinv_command(parsed_args.output_path, parsed_args.token)
109
+
110
+ case 'docs':
111
+ docs_command(
112
+ parsed_args.input_path, parsed_args.output_path,
113
+ parsed_args.title, parsed_args.include_hidden, parsed_args.notoc
114
+ )
115
+
116
+ case 'slice':
117
+ slice_command(
118
+ parsed_args.input_path, parsed_args.output_path,
119
+ parsed_args.target_length
120
+ )
121
+
122
+ case 'cctoken':
123
+ cctoken_command(parsed_args.output_path, parsed_args.scopes)
124
+
125
+ except Exception as e:
126
+ print_status(e)
127
+ return 1
128
+
129
+ return 0
130
+
131
+ if __name__ == '__main__':
132
+ sys.exit(main())
@@ -0,0 +1,132 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import websocket
5
+ from dfpyre import DFTemplate
6
+
7
+
8
+ CODECLIENT_URL = 'ws://localhost:31375'
9
+
10
+ BASE64_REGEX = re.compile(r'^[A-Za-z0-9+/]+={0,2}$')
11
+
12
+ class TemplateParsingError(Exception):
13
+ """Exception class for DFTemplate parsing errors"""
14
+
15
+ class NoTemplatesError(Exception):
16
+ """Exception class for empty template list"""
17
+
18
+
19
+ def print_status(*args, **kwargs):
20
+ """Prints a message to stderr"""
21
+ print(*args, **kwargs, file=sys.stderr)
22
+
23
+
24
+ def connect_to_codeclient(scopes: str|None=None, token: str|None=None) -> websocket.WebSocket:
25
+ """
26
+ Tries to connect to the CodeClient websocket server with the specified scopes.
27
+
28
+ Args:
29
+ scopes: The scopes to request
30
+ token: The CodeClient authentication token to use
31
+
32
+ Returns:
33
+ The connected websocket
34
+
35
+ Raises:
36
+ ConnectionRefusedError: If connection to the server could not be established
37
+ PermissionError: If scope authentication fails
38
+ """
39
+ ws = websocket.WebSocket()
40
+ ws.connect(CODECLIENT_URL)
41
+
42
+ print_status('Connected to CodeClient.')
43
+
44
+ if token:
45
+ ws.send(f'token {token}')
46
+ auth_message = ws.recv()
47
+ elif scopes:
48
+ print_status('Please run /auth in game.')
49
+ ws.send(f'scopes {scopes}')
50
+ auth_message = ws.recv()
51
+
52
+ if (token or scopes):
53
+ if auth_message != 'auth':
54
+ raise PermissionError('Failed to authenticate.')
55
+ else:
56
+ print_status('Authentication successful.')
57
+
58
+ return ws
59
+
60
+
61
+ def parse_templates_from_string(templates: str) -> list[DFTemplate]:
62
+ """
63
+ Parses a newline-delimited string of template codes into a list of templates.
64
+
65
+ Args:
66
+ templates: The string of templates to parse
67
+
68
+ Returns:
69
+ The list of parsed templates
70
+
71
+ Raises:
72
+ ValueError: If a template code is not a valid base64 string
73
+ TemplateParsingError: If a template failed to parse
74
+ """
75
+ template_codes = templates.split('\n')
76
+
77
+ for i, template_code in enumerate(template_codes):
78
+ if not BASE64_REGEX.match(template_code):
79
+ raise ValueError(f'Template code at line {i+1} is not a base64 string.')
80
+
81
+ try:
82
+ return [DFTemplate.from_code(c) for c in template_codes]
83
+ except Exception as e:
84
+ raise TemplateParsingError(f'Error while parsing template: {e}') from None
85
+
86
+
87
+ def read_input_file(path: str) -> str:
88
+ """
89
+ Returns the string content of the file at a specified path.
90
+ If the path is a hyphen ('-'), then input will be read from stdin.
91
+
92
+ Args:
93
+ path: The file path to read
94
+
95
+ Returns:
96
+ The file content or input from stdin
97
+
98
+ Raises:
99
+ FileNotFoundError: If the path is not a file or the file doesn't exist
100
+ OSError: If the file was unable to be opened or read
101
+ """
102
+ if path == '-':
103
+ try:
104
+ input_string = sys.stdin.read()
105
+ except EOFError:
106
+ pass
107
+ return input_string.strip()
108
+
109
+ if not os.path.isfile(path):
110
+ raise FileNotFoundError(f'"{path}" is not a file.')
111
+
112
+ with open(path, 'r') as f:
113
+ return f.read()
114
+
115
+
116
+ def write_output_file(path: str, content: str):
117
+ """
118
+ Writes string content to a specified file.
119
+ If the file path is a hyphen ('-') then the content will be printed to stdout.
120
+
121
+ Args:
122
+ path: The file path to write to
123
+ content: The string content to write
124
+
125
+ Raises:
126
+ OSError: If the file was unable to be accessed
127
+ """
128
+ if path == '-':
129
+ print(content, end='')
130
+ else:
131
+ with open(path, 'w') as f:
132
+ f.write(content)