pyrecli 0.2.0__tar.gz → 0.3.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.
pyrecli-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrecli
3
+ Version: 0.3.0
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.9.5)
18
+ Requires-Dist: pytest (>=9.0.2) ; extra == "dev"
19
+ Requires-Dist: rapidnbt (>=1.3.3)
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`.
93
+ pyrecli rename templates.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 plotData gameData -s game
98
+ ```
99
+
100
+ By default, this modifies the input file in-place, but you can use the `--output_path` flag to output the renamed templates to a new file:
101
+ ```sh
102
+ pyrecli rename templates.dfts foo bar --output_path renamed_templates.dfts
103
+ ```
104
+
105
+ ### Script
106
+
107
+ Generates Python scripts from template data.
108
+
109
+ Example:
110
+ ```sh
111
+ # Convert templates into individual scripts and store them in directory `plot_templates`
112
+ pyrecli script templates.dfts plot_templates
113
+
114
+ # Convert templates into scripts and put them into a single file `plot_templates.py`
115
+ pyrecli script templates.dfts plot_templates.py --onefile
116
+ ```
117
+
118
+
119
+ ### Grabinv
120
+
121
+ **[Requires CodeClient]**
122
+
123
+ Scans your inventory for templates and saves them to a file.
124
+
125
+ Example:
126
+ ```sh
127
+ # Save inventory templates to `templates.dfts`
128
+ pyrecli grabinv templates.dfts
129
+ ```
130
+
131
+
132
+ ### Docs
133
+
134
+ Generates a Markdown documentation file for a list of templates.
135
+
136
+ Example:
137
+ ```sh
138
+ # Generate documentation and save it to `plot_docs.md`
139
+ pyrecli docs templates.dfts plot_docs.md "My Plot Docs"
140
+ ```
141
+
142
+
143
+ ### Slice
144
+
145
+ Slices a single template into multiple smaller templates.
146
+ This is useful for resizing templates to fit on a smaller plot.
147
+
148
+ If multiple templates are passed, only the first one will be used.
149
+
150
+ **NOTE: This feature is not fully implemented yet. Any templates with Control::Return blocks may not work properly if sliced.**
151
+
152
+ Example:
153
+ ```sh
154
+ # Slices the first template in `templates.dfts` with a target length of 50 and stores them in `sliced_templates.dfts`
155
+ pyrecli slice templates.dfts sliced_templates.dfts 50
156
+ ```
157
+
158
+
159
+ ### CCToken
160
+
161
+ Returns a CodeClient authentication token that can be used in commands that require CodeClient authorization.
162
+ This is useful for reducing the amount of times you need to run `/auth`.
163
+
164
+ Example:
165
+ ```sh
166
+ # Get a token with the read_plot and inventory scopes
167
+ pyrecli cctoken mytoken.txt "read_plot inventory"
168
+ ```
169
+
170
+
171
+ ### Command Chaining
172
+
173
+ You can combine the pipe operator (`|`) with hyphen (`-`) file paths to chain multiple commands together.
174
+
175
+ Example:
176
+ ```sh
177
+ # Scans the plot, renames a variable, then sends renamed templates back to DiamondFire
178
+ pyrecli scan - | pyrecli rename - foo bar | pyrecli send -
179
+
180
+ ```
@@ -0,0 +1,157 @@
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`.
70
+ pyrecli rename templates.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 plotData gameData -s game
75
+ ```
76
+
77
+ By default, this modifies the input file in-place, but you can use the `--output_path` flag to output the renamed templates to a new file:
78
+ ```sh
79
+ pyrecli rename templates.dfts foo bar --output_path renamed_templates.dfts
80
+ ```
81
+
82
+ ### Script
83
+
84
+ Generates Python scripts from template data.
85
+
86
+ Example:
87
+ ```sh
88
+ # Convert templates into individual scripts and store them in directory `plot_templates`
89
+ pyrecli script templates.dfts plot_templates
90
+
91
+ # Convert templates into scripts and put them into a single file `plot_templates.py`
92
+ pyrecli script templates.dfts plot_templates.py --onefile
93
+ ```
94
+
95
+
96
+ ### Grabinv
97
+
98
+ **[Requires CodeClient]**
99
+
100
+ Scans your inventory for templates and saves them to a file.
101
+
102
+ Example:
103
+ ```sh
104
+ # Save inventory templates to `templates.dfts`
105
+ pyrecli grabinv templates.dfts
106
+ ```
107
+
108
+
109
+ ### Docs
110
+
111
+ Generates a Markdown documentation file for a list of templates.
112
+
113
+ Example:
114
+ ```sh
115
+ # Generate documentation and save it to `plot_docs.md`
116
+ pyrecli docs templates.dfts plot_docs.md "My Plot Docs"
117
+ ```
118
+
119
+
120
+ ### Slice
121
+
122
+ Slices a single template into multiple smaller templates.
123
+ This is useful for resizing templates to fit on a smaller plot.
124
+
125
+ If multiple templates are passed, only the first one will be used.
126
+
127
+ **NOTE: This feature is not fully implemented yet. Any templates with Control::Return blocks may not work properly if sliced.**
128
+
129
+ Example:
130
+ ```sh
131
+ # Slices the first template in `templates.dfts` with a target length of 50 and stores them in `sliced_templates.dfts`
132
+ pyrecli slice templates.dfts sliced_templates.dfts 50
133
+ ```
134
+
135
+
136
+ ### CCToken
137
+
138
+ Returns a CodeClient authentication token that can be used in commands that require CodeClient authorization.
139
+ This is useful for reducing the amount of times you need to run `/auth`.
140
+
141
+ Example:
142
+ ```sh
143
+ # Get a token with the read_plot and inventory scopes
144
+ pyrecli cctoken mytoken.txt "read_plot inventory"
145
+ ```
146
+
147
+
148
+ ### Command Chaining
149
+
150
+ You can combine the pipe operator (`|`) with hyphen (`-`) file paths to chain multiple commands together.
151
+
152
+ Example:
153
+ ```sh
154
+ # Scans the plot, renames a variable, then sends renamed templates back to DiamondFire
155
+ pyrecli scan - | pyrecli rename - foo bar | pyrecli send -
156
+
157
+ ```
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "pyrecli"
3
+ version = "0.3.0"
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.9.5",
15
+ "rapidnbt>=1.3.3"
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"
@@ -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)
@@ -1,5 +1,4 @@
1
1
  from typing import Literal, TypedDict
2
- from result import Result, Err
3
2
  from dfpyre import DFTemplate, Item, Parameter
4
3
  from mcitemlib.itemlib import MCItemlibException
5
4
  from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string
@@ -24,15 +23,9 @@ class TemplateDocData(TypedDict):
24
23
  doc_lines: list[str]
25
24
 
26
25
 
27
- def docs_command(input_path: str, output_path: str, title: str, include_hidden: bool, omit_toc: bool) -> Result[None, str]:
28
- input_result = read_input_file(input_path)
29
- if input_result.is_err():
30
- return Err(input_result.err_value)
31
-
32
- templates_result = parse_templates_from_string(input_result.ok_value)
33
- if templates_result.is_err():
34
- return Err(templates_result.err_value)
35
- templates = templates_result.ok_value
26
+ def docs_command(input_path: str, output_path: str, title: str, include_hidden: bool, omit_toc: bool):
27
+ templates_string = read_input_file(input_path)
28
+ templates = parse_templates_from_string(templates_string)
36
29
 
37
30
  def get_function_name(template: DFTemplate) -> str:
38
31
  first_block = template.codeblocks[0]
@@ -72,7 +65,7 @@ def docs_command(input_path: str, output_path: str, title: str, include_hidden:
72
65
  lore_text = [escape_md(l.to_string()) for l in first_arg.get_lore()]
73
66
  if lore_text:
74
67
  template_doc_lines.extend(lore_text)
75
- except (MCItemlibException, AttributeError):
68
+ except:
76
69
  # There are so many things that can go wrong here due to various legacy
77
70
  # item formats and weird MC string edge cases, so we can just skip
78
71
  # if there's a problem.
@@ -120,5 +113,4 @@ def docs_command(input_path: str, output_path: str, title: str, include_hidden:
120
113
  output_lines.extend(doc_data['doc_lines'])
121
114
  output_lines.append('')
122
115
 
123
- write_result = write_output_file(output_path, '\n'.join(output_lines))
124
- return write_result
116
+ 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))
@@ -1,7 +1,6 @@
1
1
  from typing import Literal
2
2
  import re
3
- from result import Result, Err
4
- from dfpyre import Variable, Number, String, Text
3
+ from dfpyre import Variable, Number, String, Text, Parameter
5
4
  from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string
6
5
 
7
6
 
@@ -22,33 +21,34 @@ def rename_var_in_text_code(s: str, var_to_rename: str, new_var_name: str):
22
21
 
23
22
  def rename_command(input_path: str, output_path: str|None,
24
23
  var_to_rename: str, new_var_name: str,
25
- var_to_rename_scope: Literal['game', 'saved', 'local', 'line']|None) -> Result[None, str]:
24
+ var_to_rename_scope: Literal['game', 'saved', 'local', 'line']|None):
26
25
 
27
- input_result = read_input_file(input_path)
28
- if input_result.is_err():
29
- return Err(input_result.err_value)
30
-
31
- templates_result = parse_templates_from_string(input_result.ok_value)
32
- if templates_result.is_err():
33
- return Err(templates_result.err_value)
34
- templates = templates_result.ok_value
26
+ templates_string = read_input_file(input_path)
27
+ templates = parse_templates_from_string(templates_string)
35
28
 
36
29
  for template in templates:
37
30
  for codeblock in template.codeblocks:
38
31
  for argument in codeblock.args:
32
+ # Try to rename variable
39
33
  if isinstance(argument, Variable):
40
34
  if argument.name == var_to_rename and (var_to_rename_scope is None or argument.scope == var_to_rename_scope):
41
35
  argument.name = new_var_name
42
36
  argument.name = rename_var_in_text_code(argument.name, var_to_rename, new_var_name)
43
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
44
  elif isinstance(argument, (Number, String, Text)) and isinstance(argument.value, str):
45
45
  argument.value = rename_var_in_text_code(argument.value, var_to_rename, new_var_name)
46
46
 
47
+ # Check for text codes in function calls
47
48
  if codeblock.type in {'call_func', 'start_process'}:
48
49
  new_data = rename_var_in_text_code(codeblock.data.get('data'), var_to_rename, new_var_name)
49
50
  codeblock.data['data'] = new_data
50
51
 
51
52
  new_file_content = '\n'.join(t.build() for t in templates)
52
53
  write_path = output_path if output_path else input_path
53
- write_result = write_output_file(write_path, new_file_content)
54
- return write_result
54
+ write_output_file(write_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)
@@ -1,26 +1,20 @@
1
1
  import os
2
- from result import Result, Ok, Err
3
2
  from dfpyre import DFTemplate
4
3
  from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string
5
4
 
6
5
 
7
- def write_to_directory(dir_name: str, templates: list[DFTemplate], flags: dict[str, int|bool]) -> Result[None, str]:
6
+ def write_to_directory(dir_name: str, templates: list[DFTemplate], flags: dict[str, int|bool]):
8
7
  if not os.path.isdir(dir_name):
9
8
  os.mkdir(dir_name)
10
9
 
11
10
  for template in templates:
12
11
  script_path = f'{dir_name}/{template._get_template_name()}.py'
13
12
  script_string = template.generate_script(**flags)
14
- try:
15
- with open(script_path, 'w') as f:
16
- f.write(script_string)
17
- except OSError as e:
18
- return Err(str(e))
19
-
20
- return Ok(None)
13
+ with open(script_path, 'w') as f:
14
+ f.write(script_string)
21
15
 
22
16
 
23
- def write_to_single_file(file_path: str, templates: list[DFTemplate], flags: dict[str, int|bool]) -> Result[None, str]:
17
+ def write_to_single_file(file_path: str, templates: list[DFTemplate], flags: dict[str, int|bool]):
24
18
  file_content = []
25
19
  for i, template in enumerate(templates):
26
20
  if i == 0:
@@ -29,18 +23,12 @@ def write_to_single_file(file_path: str, templates: list[DFTemplate], flags: dic
29
23
  template_script = template.generate_script(include_import=False, assign_variable=True, **flags)
30
24
  file_content.append(template_script)
31
25
 
32
- return write_output_file(file_path, '\n\n'.join(file_content))
33
-
26
+ write_output_file(file_path, '\n\n'.join(file_content))
34
27
 
35
- def script_command(input_path: str, output_path: str, one_file: bool, flags: dict[str, int|bool]) -> Result[None, str]:
36
- input_result = read_input_file(input_path)
37
- if input_result.is_err():
38
- return Err(input_result.err_value)
39
28
 
40
- templates_result = parse_templates_from_string(input_result.ok_value)
41
- if templates_result.is_err():
42
- return Err(templates_result.err_value)
43
- templates = templates_result.ok_value
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)
44
32
 
45
33
  if one_file or output_path == '-':
46
34
  return write_to_single_file(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))
@@ -9,6 +9,15 @@ from pyrecli.command.rename import rename_command
9
9
  from pyrecli.command.grabinv import grabinv_command
10
10
  from pyrecli.command.docs import docs_command
11
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
12
21
 
13
22
 
14
23
  def slice_target_length(value):
@@ -19,13 +28,14 @@ def slice_target_length(value):
19
28
  return ivalue
20
29
 
21
30
 
22
- def main():
31
+ def main() -> int:
23
32
  parser = argparse.ArgumentParser(prog='pyrecli', description='Command line utilities for DiamondFire templates')
24
33
  parser.add_argument('--version', '-v', action='version', version=f'pyrecli {importlib.metadata.version('pyrecli')}')
25
34
  subparsers = parser.add_subparsers(dest='command', help='Available commands:', required=True, metavar='<command>')
26
35
 
27
36
  parser_scan = subparsers.add_parser('scan', help='Scan the current plot templates with CodeClient')
28
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)
29
39
 
30
40
  parser_send = subparsers.add_parser('send', help='Send templates to DiamondFire with CodeClient')
31
41
  parser_send.add_argument('input_path', help='The file containing template data', type=str)
@@ -40,20 +50,21 @@ def main():
40
50
  parser_script.add_argument('--preserve_slots', '-s', help='Save the positions of items within chests', action='store_true')
41
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')
42
52
 
43
- parser_rename = subparsers.add_parser('rename', help='Rename a variable')
53
+ parser_rename = subparsers.add_parser('rename', help='Rename all occurrences of a variable')
44
54
  parser_rename.add_argument('input_path', help='The file containing template data', type=str)
45
55
  parser_rename.add_argument('var_to_rename', help='The variable to rename', type=str)
46
56
  parser_rename.add_argument('new_var_name', help='The new name for the variable', type=str)
47
- parser_rename.add_argument('--var_to_rename_scope', '-s', help='The scope to match', type=str, default=None)
57
+ parser_rename.add_argument('--var_scope', '-s', help='The scope to match', type=rename_target_scope, default=None)
48
58
  parser_rename.add_argument('--output_path', '-o', help='The file to output to', type=str, default=None)
49
59
 
50
60
  parser_grabinv = subparsers.add_parser('grabinv', help='Save all templates in the inventory to a file with CodeClient')
51
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)
52
63
 
53
64
  parser_docs = subparsers.add_parser('docs', help='Generate markdown documentation from template data')
54
65
  parser_docs.add_argument('input_path', help='The file containing template data', type=str)
55
66
  parser_docs.add_argument('output_path', help='The file to output to', type=str)
56
- parser_docs.add_argument('title', help='The title for the docs', type=str)
67
+ parser_docs.add_argument('--title', '-t', help='The title for the docs', type=str, default='Template Docs')
57
68
  parser_docs.add_argument('--include_hidden', '-ih', help='Include hidden functions and processes', action='store_true')
58
69
  parser_docs.add_argument('--notoc', help='Omit the table of contents', action='store_true')
59
70
 
@@ -61,52 +72,61 @@ def main():
61
72
  parser_slice.add_argument('input_path', help='The file containing template data', type=str)
62
73
  parser_slice.add_argument('output_path', help='The file to output template data to', type=str)
63
74
  parser_slice.add_argument('target_length', help='The maximum length of each sliced template', type=slice_target_length)
64
-
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
+
65
81
  parsed_args = parser.parse_args()
66
82
 
67
- match parsed_args.command:
68
- case 'scan':
69
- command_result = scan_command(parsed_args.output_path)
70
-
71
- case 'send':
72
- command_result = send_command(parsed_args.input_path)
73
-
74
- case 'script':
75
- scriptgen_flags = {
76
- 'indent_size': parsed_args.indent_size,
77
- 'literal_shorthand': parsed_args.literal_shorthand,
78
- 'var_shorthand': parsed_args.var_shorthand,
79
- 'preserve_slots': parsed_args.preserve_slots,
80
- 'build_and_send': parsed_args.build_and_send
81
- }
82
- command_result = script_command(parsed_args.input_path, parsed_args.output_path, parsed_args.onefile, scriptgen_flags)
83
-
84
- case 'rename':
85
- command_result = rename_command(
86
- parsed_args.input_path, parsed_args.output_path,
87
- parsed_args.var_to_rename, parsed_args.new_var_name, parsed_args.var_to_rename_scope
88
- )
89
-
90
- case 'grabinv':
91
- command_result = grabinv_command(parsed_args.output_path)
92
-
93
- case 'docs':
94
- command_result = docs_command(
95
- parsed_args.input_path, parsed_args.output_path,
96
- parsed_args.title, parsed_args.include_hidden, parsed_args.notoc
97
- )
98
-
99
- case 'slice':
100
- command_result = slice_command(
101
- parsed_args.input_path, parsed_args.output_path,
102
- parsed_args.target_length
103
- )
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)
104
124
 
105
- if command_result.is_err():
106
- print(command_result.err_value)
107
- sys.exit(1)
125
+ except Exception as e:
126
+ print_status(e)
127
+ return 1
108
128
 
109
- sys.exit(0)
129
+ return 0
110
130
 
111
131
  if __name__ == '__main__':
112
- 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)
pyrecli-0.2.0/PKG-INFO DELETED
@@ -1,66 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: pyrecli
3
- Version: 0.2.0
4
- Summary: Command line utilities for DiamondFire templates
5
- Home-page: https://github.com/Amp63/pyrecli
6
- License: MIT
7
- Keywords: diamondfire,minecraft,template,cli
8
- Author: Amp
9
- Requires-Python: >=3.10,<3.13
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Project-URL: Repository, https://github.com/Amp63/pyrecli
16
- Description-Content-Type: text/markdown
17
-
18
- # pyrecli
19
-
20
- Command line utilities for DiamondFire templates
21
-
22
- ## Installation
23
-
24
- Run the following command in a terminal:
25
-
26
- ```sh
27
- pip install pyrecli
28
- ```
29
-
30
- ## Commands
31
-
32
- - `scan`: Scan all templates on the plot and dump them to a text file (requires [CodeClient](github.com/DFOnline/CodeClient))
33
- - `send`: Send template items to DiamondFire (requires [CodeClient](github.com/DFOnline/CodeClient))
34
- - `rename`: Rename all occurences of a variable (including text codes)
35
- - `script`: Generate python scripts from template data
36
- - `grabinv`: Save all templates in your Minecraft inventory to a file (requires [CodeClient](github.com/DFOnline/CodeClient))
37
- - `docs`: Generate markdown documentation from template data
38
-
39
-
40
- ## What is this useful for?
41
-
42
- - Backing up a plot
43
- - Getting an accurate text representation of DF code
44
- - Open sourcing
45
- - Version control
46
- - Large scale refactoring
47
-
48
-
49
- ## Example Usage
50
-
51
- These two commands will scan your plot, convert each template into a python script, then place the scripts into a directory called `myplot`.
52
-
53
- ```sh
54
- pyrecli scan templates.dfts
55
- pyrecli script templates.dfts myplot
56
- ```
57
-
58
- If you prefer the templates to be outputted to a single file, use the `--onefile` flag:
59
-
60
- ```sh
61
- pyrecli scan templates.dfts
62
- pyrecli script templates.dfts myplot.py --onefile
63
- ```
64
-
65
- For more information about generating scripts, run `pyrecli script -h`.
66
-
pyrecli-0.2.0/README.md DELETED
@@ -1,48 +0,0 @@
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
-
22
-
23
- ## What is this useful for?
24
-
25
- - Backing up a plot
26
- - Getting an accurate text representation of DF code
27
- - Open sourcing
28
- - Version control
29
- - Large scale refactoring
30
-
31
-
32
- ## Example Usage
33
-
34
- These two commands will scan your plot, convert each template into a python script, then place the scripts into a directory called `myplot`.
35
-
36
- ```sh
37
- pyrecli scan templates.dfts
38
- pyrecli script templates.dfts myplot
39
- ```
40
-
41
- If you prefer the templates to be outputted to a single file, use the `--onefile` flag:
42
-
43
- ```sh
44
- pyrecli scan templates.dfts
45
- pyrecli script templates.dfts myplot.py --onefile
46
- ```
47
-
48
- For more information about generating scripts, run `pyrecli script -h`.
@@ -1,22 +0,0 @@
1
- [tool.poetry]
2
- name = "pyrecli"
3
- version = "0.2.0"
4
- description = "Command line utilities for DiamondFire templates"
5
- authors = ["Amp"]
6
- readme = "README.md"
7
- license = "MIT"
8
- repository = "https://github.com/Amp63/pyrecli"
9
- keywords = ["diamondfire", "minecraft", "template", "cli"]
10
-
11
-
12
- [tool.poetry.dependencies]
13
- python = ">=3.10,<3.13"
14
-
15
-
16
- [tool.poetry.scripts]
17
- pyrecli = "pyrecli.pyrecli:main"
18
-
19
-
20
- [build-system]
21
- requires = ["poetry-core"]
22
- build-backend = "poetry.core.masonry.api"
@@ -1,49 +0,0 @@
1
- import json
2
- from result import Result, Ok, Err
3
- import amulet_nbt
4
- from amulet_nbt import CompoundTag, StringTag
5
- from pyrecli.util import write_output_file, connect_to_codeclient
6
-
7
-
8
- def grabinv_command(output_path: str) -> Result[None, str]:
9
- ws_result = connect_to_codeclient('inventory')
10
- if ws_result.is_err():
11
- return Err(ws_result.err_value)
12
- ws = ws_result.ok_value
13
-
14
- ws.send('inv')
15
- inventory = ws.recv()
16
- inventory_nbt = amulet_nbt.from_snbt(inventory)
17
-
18
- template_codes: list[str] = []
19
- for tag in inventory_nbt:
20
- components: CompoundTag = tag.get('components')
21
- if components is None:
22
- continue
23
-
24
- custom_data: CompoundTag = components.get('minecraft:custom_data')
25
- if custom_data is None:
26
- continue
27
-
28
- pbv_tag: CompoundTag = custom_data.get('PublicBukkitValues')
29
- if pbv_tag is None:
30
- continue
31
-
32
- code_template_data: StringTag = pbv_tag.get('hypercube:codetemplatedata')
33
- if code_template_data is None:
34
- continue
35
-
36
- code_template_json = json.loads(str(code_template_data))
37
-
38
- template_code = code_template_json.get('code')
39
- if template_code:
40
- template_codes.append(template_code)
41
-
42
- if not template_codes:
43
- return Err('Could not find any templates in the inventory.')
44
-
45
- output_result = write_output_file(output_path, '\n'.join(template_codes))
46
- if output_result.is_err():
47
- return output_result
48
-
49
- return Ok(None)
@@ -1,19 +0,0 @@
1
- from result import Result, Err
2
- from pyrecli.util import write_output_file, connect_to_codeclient
3
-
4
-
5
- def scan_command(output_path: str) -> Result[None, str]:
6
- ws_result = connect_to_codeclient('read_plot')
7
- if ws_result.is_err():
8
- return Err(ws_result.err_value)
9
- ws = ws_result.ok_value
10
-
11
- print('Scanning plot...')
12
- ws.send('scan')
13
-
14
- scan_results = ws.recv()
15
- print('Done.')
16
- ws.close()
17
-
18
- write_result = write_output_file(output_path, scan_results)
19
- return write_result
@@ -1,26 +0,0 @@
1
- from result import Result, Ok, Err
2
- from pyrecli.util import connect_to_codeclient, read_input_file, parse_templates_from_string
3
-
4
-
5
- def send_command(input_path: str) -> Result[None, str]:
6
- input_result = read_input_file(input_path)
7
- if input_result.is_err():
8
- return Err(input_result.err_value)
9
-
10
- templates_result = parse_templates_from_string(input_result.ok_value)
11
- if templates_result.is_err():
12
- return Err(templates_result.err_value)
13
- templates = templates_result.ok_value
14
-
15
- ws_result = connect_to_codeclient()
16
- if ws_result.is_err():
17
- return Err(ws_result.err_value)
18
- ws = ws_result.ok_value
19
-
20
- for template in templates:
21
- item = template.generate_template_item()
22
- ws.send(f'give {item.get_snbt()}')
23
-
24
- ws.close()
25
- print(f'Sent {len(templates)} template{"s" if len(templates) != 1 else ''} successfully.')
26
- return Ok(None)
@@ -1,21 +0,0 @@
1
- from result import Result, Err
2
- from pyrecli.util import read_input_file, write_output_file, parse_templates_from_string
3
-
4
- def slice_command(input_path: str, output_path: str, target_length: int) -> Result[None, str]:
5
- input_result = read_input_file(input_path)
6
- if input_result.is_err():
7
- return Err(input_result.err_value)
8
-
9
- templates_result = parse_templates_from_string(input_result.ok_value)
10
- if templates_result.is_err():
11
- return Err(templates_result.err_value)
12
- templates = templates_result.ok_value
13
-
14
- if not templates:
15
- return Err(f'Could not find any templates in {input_path}')
16
-
17
- first_template = templates[0]
18
- sliced_templates = first_template.slice(target_length)
19
- built_templates = [t.build() for t in sliced_templates]
20
-
21
- return write_output_file(output_path, '\n'.join(built_templates))
@@ -1,76 +0,0 @@
1
- import os
2
- import re
3
- import sys
4
- from result import Result, Ok, Err
5
- import websocket
6
- from dfpyre import DFTemplate
7
-
8
-
9
- CODECLIENT_URL = 'ws://localhost:31375'
10
-
11
- BASE64_REGEX = re.compile(r'^[A-Za-z0-9+/]+={0,2}$')
12
-
13
-
14
- def connect_to_codeclient(scopes: str|None=None) -> Result[websocket.WebSocket, str]:
15
- ws = websocket.WebSocket()
16
- try:
17
- ws.connect(CODECLIENT_URL)
18
- except ConnectionRefusedError:
19
- return Err('Failed to connect to CodeClient.')
20
-
21
- print('Connected to CodeClient.')
22
-
23
- if scopes:
24
- print('Please run /auth in game.')
25
- ws.send(f'scopes {scopes}')
26
- auth_message = ws.recv()
27
-
28
- if auth_message != 'auth':
29
- return Err('Failed to authenticate.')
30
- print('Authentication received.')
31
-
32
- return Ok(ws)
33
-
34
-
35
- def parse_templates_from_string(templates: str) -> Result[list[DFTemplate], str]:
36
- template_codes = templates.split('\n')
37
-
38
- for i, template_code in enumerate(template_codes):
39
- if not BASE64_REGEX.match(template_code):
40
- return Err(f'Template code at line {i+1} is not a base64 string.')
41
-
42
- try:
43
- return Ok([DFTemplate.from_code(c) for c in template_codes])
44
- except Exception as e:
45
- return Err(str(e))
46
-
47
-
48
- def read_input_file(path: str) -> Result[str, str]:
49
- if path == '-':
50
- try:
51
- input_string = sys.stdin.read()
52
- except EOFError:
53
- pass
54
- return Ok(input_string.strip())
55
-
56
- if not os.path.isfile(path):
57
- return Err(f'"{path}" is not a file.')
58
-
59
- try:
60
- with open(path, 'r') as f:
61
- return Ok(f.read())
62
- except OSError as e:
63
- return Err(str(e))
64
-
65
-
66
- def write_output_file(path: str, content: str) -> Result[None, str]:
67
- if path == '-':
68
- print(content, end='')
69
- else:
70
- try:
71
- with open(path, 'w') as f:
72
- f.write(content)
73
- except OSError as e:
74
- return Err(str(e))
75
-
76
- return Ok(None)
File without changes
File without changes