pyrecli 0.2.1__tar.gz → 0.3.1__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.
@@ -1,3 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrecli
3
+ Version: 0.3.1
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.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
+
1
24
  # pyrecli
2
25
 
3
26
  Command line utilities for DiamondFire templates
@@ -19,6 +42,7 @@ pip install pyrecli
19
42
  - `grabinv`: Save all templates in your Minecraft inventory to a file (requires [CodeClient](github.com/DFOnline/CodeClient))
20
43
  - `docs`: Generate markdown documentation from template data
21
44
  - `slice`: Slice a template into multiple smaller templates
45
+ - `cctoken`: Get a reusable CodeClient authentication token
22
46
 
23
47
 
24
48
  ## What is this useful for?
@@ -49,8 +73,7 @@ pyrecli scan templates.dfts
49
73
 
50
74
  **[Requires CodeClient]**
51
75
 
52
- Sends all templates in a file back to DiamondFire.
53
- Essentially the reverse of `scan`.
76
+ Sends all templates in a file back to your inventory.
54
77
 
55
78
  Example:
56
79
  ```sh
@@ -131,3 +154,27 @@ Example:
131
154
  # Slices the first template in `templates.dfts` with a target length of 50 and stores them in `sliced_templates.dfts`
132
155
  pyrecli slice templates.dfts sliced_templates.dfts 50
133
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
+ ```
@@ -1,21 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: pyrecli
3
- Version: 0.2.1
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
- Requires-Dist: dfpyre (>=0.9.3,<0.10.0)
16
- Project-URL: Repository, https://github.com/Amp63/pyrecli
17
- Description-Content-Type: text/markdown
18
-
19
1
  # pyrecli
20
2
 
21
3
  Command line utilities for DiamondFire templates
@@ -37,6 +19,7 @@ pip install pyrecli
37
19
  - `grabinv`: Save all templates in your Minecraft inventory to a file (requires [CodeClient](github.com/DFOnline/CodeClient))
38
20
  - `docs`: Generate markdown documentation from template data
39
21
  - `slice`: Slice a template into multiple smaller templates
22
+ - `cctoken`: Get a reusable CodeClient authentication token
40
23
 
41
24
 
42
25
  ## What is this useful for?
@@ -67,8 +50,7 @@ pyrecli scan templates.dfts
67
50
 
68
51
  **[Requires CodeClient]**
69
52
 
70
- Sends all templates in a file back to DiamondFire.
71
- Essentially the reverse of `scan`.
53
+ Sends all templates in a file back to your inventory.
72
54
 
73
55
  Example:
74
56
  ```sh
@@ -150,3 +132,26 @@ Example:
150
132
  pyrecli slice templates.dfts sliced_templates.dfts 50
151
133
  ```
152
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.1"
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.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"
@@ -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,8 @@ 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
12
14
 
13
15
 
14
16
  def rename_target_scope(value):
@@ -18,7 +20,6 @@ def rename_target_scope(value):
18
20
  return value
19
21
 
20
22
 
21
-
22
23
  def slice_target_length(value):
23
24
  MINIMUM_LENGTH = 5
24
25
  ivalue = int(value)
@@ -27,13 +28,14 @@ def slice_target_length(value):
27
28
  return ivalue
28
29
 
29
30
 
30
- def main():
31
+ def main() -> int:
31
32
  parser = argparse.ArgumentParser(prog='pyrecli', description='Command line utilities for DiamondFire templates')
32
33
  parser.add_argument('--version', '-v', action='version', version=f'pyrecli {importlib.metadata.version('pyrecli')}')
33
34
  subparsers = parser.add_subparsers(dest='command', help='Available commands:', required=True, metavar='<command>')
34
35
 
35
36
  parser_scan = subparsers.add_parser('scan', help='Scan the current plot templates with CodeClient')
36
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)
37
39
 
38
40
  parser_send = subparsers.add_parser('send', help='Send templates to DiamondFire with CodeClient')
39
41
  parser_send.add_argument('input_path', help='The file containing template data', type=str)
@@ -48,20 +50,21 @@ def main():
48
50
  parser_script.add_argument('--preserve_slots', '-s', help='Save the positions of items within chests', action='store_true')
49
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')
50
52
 
51
- parser_rename = subparsers.add_parser('rename', help='Rename a variable')
53
+ parser_rename = subparsers.add_parser('rename', help='Rename all occurrences of a variable')
52
54
  parser_rename.add_argument('input_path', help='The file containing template data', type=str)
53
55
  parser_rename.add_argument('var_to_rename', help='The variable to rename', type=str)
54
56
  parser_rename.add_argument('new_var_name', help='The new name for the variable', type=str)
55
- parser_rename.add_argument('--var_to_rename_scope', '-s', help='The scope to match', type=rename_target_scope, default=None)
57
+ parser_rename.add_argument('--var_scope', '-s', help='The scope to match', type=rename_target_scope, default=None)
56
58
  parser_rename.add_argument('--output_path', '-o', help='The file to output to', type=str, default=None)
57
59
 
58
60
  parser_grabinv = subparsers.add_parser('grabinv', help='Save all templates in the inventory to a file with CodeClient')
59
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)
60
63
 
61
64
  parser_docs = subparsers.add_parser('docs', help='Generate markdown documentation from template data')
62
65
  parser_docs.add_argument('input_path', help='The file containing template data', type=str)
63
66
  parser_docs.add_argument('output_path', help='The file to output to', type=str)
64
- 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')
65
68
  parser_docs.add_argument('--include_hidden', '-ih', help='Include hidden functions and processes', action='store_true')
66
69
  parser_docs.add_argument('--notoc', help='Omit the table of contents', action='store_true')
67
70
 
@@ -70,51 +73,60 @@ def main():
70
73
  parser_slice.add_argument('output_path', help='The file to output template data to', type=str)
71
74
  parser_slice.add_argument('target_length', help='The maximum length of each sliced template', type=slice_target_length)
72
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
+
73
81
  parsed_args = parser.parse_args()
74
82
 
75
- match parsed_args.command:
76
- case 'scan':
77
- command_result = scan_command(parsed_args.output_path)
78
-
79
- case 'send':
80
- command_result = send_command(parsed_args.input_path)
81
-
82
- case 'script':
83
- scriptgen_flags = {
84
- 'indent_size': parsed_args.indent_size,
85
- 'literal_shorthand': parsed_args.literal_shorthand,
86
- 'var_shorthand': parsed_args.var_shorthand,
87
- 'preserve_slots': parsed_args.preserve_slots,
88
- 'build_and_send': parsed_args.build_and_send
89
- }
90
- command_result = script_command(parsed_args.input_path, parsed_args.output_path, parsed_args.onefile, scriptgen_flags)
91
-
92
- case 'rename':
93
- command_result = rename_command(
94
- parsed_args.input_path, parsed_args.output_path,
95
- parsed_args.var_to_rename, parsed_args.new_var_name, parsed_args.var_to_rename_scope
96
- )
97
-
98
- case 'grabinv':
99
- command_result = grabinv_command(parsed_args.output_path)
100
-
101
- case 'docs':
102
- command_result = docs_command(
103
- parsed_args.input_path, parsed_args.output_path,
104
- parsed_args.title, parsed_args.include_hidden, parsed_args.notoc
105
- )
106
-
107
- case 'slice':
108
- command_result = slice_command(
109
- parsed_args.input_path, parsed_args.output_path,
110
- parsed_args.target_length
111
- )
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)
112
124
 
113
- if command_result.is_err():
114
- print(command_result.err_value)
115
- sys.exit(1)
125
+ except Exception as e:
126
+ print_status(e)
127
+ return 1
116
128
 
117
- sys.exit(0)
129
+ return 0
118
130
 
119
131
  if __name__ == '__main__':
120
- 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)
@@ -1,23 +0,0 @@
1
- [tool.poetry]
2
- name = "pyrecli"
3
- version = "0.2.1"
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
- dfpyre = "^0.9.3"
15
-
16
-
17
- [tool.poetry.scripts]
18
- pyrecli = "pyrecli.pyrecli:main"
19
-
20
-
21
- [build-system]
22
- requires = ["poetry-core"]
23
- 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