pyrecli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

pyrecli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyrecli
3
+ Version: 0.1.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,<4.0
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
+ ## Commands
23
+
24
+ - `scan`: Scan all templates on the plot and dump them to a text file (requires [CodeClient](github.com/DFOnline/CodeClient))
25
+ - `send`: Send template items to DiamondFire (requires [CodeClient](github.com/DFOnline/CodeClient))
26
+ - `rename`: Rename all occurences of a variable (including text codes)
27
+ - `script`: Generate python scripts from template data
28
+ - `grabinv`: Save all templates in your Minecraft inventory to a file (requires [CodeClient](github.com/DFOnline/CodeClient))
29
+ - `docs`: Generate markdown documentation from template data
30
+
31
+
32
+ ## What is this useful for?
33
+
34
+ - Backing up a plot
35
+ - Getting an accurate text representation of DF code
36
+ - Open sourcing
37
+ - Version control
38
+ - Large scale refactoring
39
+
40
+
41
+ ## Example Usage
42
+
43
+ These two commands will scan your plot, convert each template into a python script, then place the scripts into a directory called `myplot`.
44
+
45
+ ```sh
46
+ pyrecli scan templates.dfts
47
+ pyrecli script templates.dfts myplot
48
+ ```
49
+
50
+ If you prefer the templates to be outputted to a single file, use the `--onefile` flag:
51
+
52
+ ```sh
53
+ pyrecli scan templates.dfts
54
+ pyrecli script templates.dfts myplot.py --onefile
55
+ ```
56
+
57
+ For more information about generating scripts, run `pyrecli script -h`.
58
+
@@ -0,0 +1,40 @@
1
+ # pyrecli
2
+
3
+ Command line utilities for DiamondFire templates
4
+
5
+ ## Commands
6
+
7
+ - `scan`: Scan all templates on the plot and dump them to a text file (requires [CodeClient](github.com/DFOnline/CodeClient))
8
+ - `send`: Send template items to DiamondFire (requires [CodeClient](github.com/DFOnline/CodeClient))
9
+ - `rename`: Rename all occurences of a variable (including text codes)
10
+ - `script`: Generate python scripts from template data
11
+ - `grabinv`: Save all templates in your Minecraft inventory to a file (requires [CodeClient](github.com/DFOnline/CodeClient))
12
+ - `docs`: Generate markdown documentation from template data
13
+
14
+
15
+ ## What is this useful for?
16
+
17
+ - Backing up a plot
18
+ - Getting an accurate text representation of DF code
19
+ - Open sourcing
20
+ - Version control
21
+ - Large scale refactoring
22
+
23
+
24
+ ## Example Usage
25
+
26
+ These two commands will scan your plot, convert each template into a python script, then place the scripts into a directory called `myplot`.
27
+
28
+ ```sh
29
+ pyrecli scan templates.dfts
30
+ pyrecli script templates.dfts myplot
31
+ ```
32
+
33
+ If you prefer the templates to be outputted to a single file, use the `--onefile` flag:
34
+
35
+ ```sh
36
+ pyrecli scan templates.dfts
37
+ pyrecli script templates.dfts myplot.py --onefile
38
+ ```
39
+
40
+ For more information about generating scripts, run `pyrecli script -h`.
@@ -0,0 +1,22 @@
1
+ [tool.poetry]
2
+ name = "pyrecli"
3
+ version = "0.1.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"
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"
File without changes
@@ -0,0 +1,120 @@
1
+ from typing import Literal, TypedDict
2
+ from result import Result, Ok, Err
3
+ from dfpyre import DFTemplate, Item, Parameter
4
+ from mcitemlib.itemlib import MCItemlibException
5
+ from pyrecli.util import parse_templates_from_file
6
+
7
+
8
+ STARTER_BLOCK_LOOKUP = {
9
+ 'func': 'Function',
10
+ 'process': 'Process'
11
+ }
12
+
13
+
14
+ def escape_md(s: str) -> str:
15
+ MD_CHARS = r'*`$#!&^~'
16
+ for char in MD_CHARS:
17
+ s = s.replace(char, rf'\{char}')
18
+ return s
19
+
20
+
21
+ class TemplateDocData(TypedDict):
22
+ template_type: Literal['Function', 'Process']
23
+ function_name: str
24
+ doc_lines: list[str]
25
+
26
+
27
+ def docs_command(input_path: str, output_path: str, title: str, include_hidden: bool, omit_toc: bool) -> Result[None, str]:
28
+ templates_result = parse_templates_from_file(input_path)
29
+ if templates_result.is_err():
30
+ return Err(templates_result.err_value)
31
+ templates = templates_result.ok_value
32
+
33
+ def get_function_name(template: DFTemplate) -> str:
34
+ first_block = template.codeblocks[0]
35
+ function_name = first_block.data.get('data')
36
+ return escape_md(function_name)
37
+
38
+ block_type_order = list(STARTER_BLOCK_LOOKUP.keys())
39
+ templates = [t for t in templates if t.codeblocks[0].action_name == 'dynamic']
40
+ templates.sort(key=get_function_name)
41
+ templates.sort(key=lambda t: block_type_order.index(t.codeblocks[0].type))
42
+
43
+ output_lines: list[str] = [
44
+ f'# {title}',
45
+ ''
46
+ ]
47
+
48
+ template_docs: list[TemplateDocData] = []
49
+ for template in templates:
50
+ first_block = template.codeblocks[0]
51
+
52
+ # Skip if hidden
53
+ if first_block.tags.get('Is Hidden') == 'True' and not include_hidden:
54
+ continue
55
+
56
+ # Add function / process name
57
+ template_type = STARTER_BLOCK_LOOKUP[first_block.type]
58
+ template_name = get_function_name(template)
59
+ template_doc_lines: list[str] = []
60
+
61
+ template_doc_lines.append(f'## {template_type}: {template_name}')
62
+
63
+ # Parse description
64
+ if first_block.args:
65
+ first_arg = first_block.args[0]
66
+ if isinstance(first_arg, Item):
67
+ try:
68
+ lore_text = [escape_md(l.to_string()) for l in first_arg.get_lore()]
69
+ if lore_text:
70
+ template_doc_lines.extend(lore_text)
71
+ except MCItemlibException:
72
+ pass
73
+
74
+ # Parse parameters
75
+ parameter_lines: list[str] = []
76
+ for arg in first_block.args:
77
+ if isinstance(arg, Parameter):
78
+ optional_text = "*" if arg.optional else ""
79
+ default_value_text = f'= `{escape_md(arg.default_value.__repr__())}`' if arg.default_value else ''
80
+ parameter_lines.append(f'- *`{escape_md(arg.name)}{optional_text}`*: `{arg.param_type.get_string_value()}` {default_value_text}')
81
+ if arg.description:
82
+ parameter_lines.append(f' - {escape_md(arg.description)}')
83
+ if arg.note:
84
+ parameter_lines.append(f' - {escape_md(arg.note)}')
85
+ if parameter_lines:
86
+ template_doc_lines.append('')
87
+ template_doc_lines.append('### Parameters:')
88
+ template_doc_lines.extend(parameter_lines)
89
+
90
+ doc_data = TemplateDocData(template_type=template_type, function_name=template_name, doc_lines=template_doc_lines)
91
+ template_docs.append(doc_data)
92
+
93
+
94
+ # Add table of contents
95
+ def add_toc_group(doc_data_list: list[TemplateDocData], group_title: str):
96
+ if doc_data_list:
97
+ output_lines.append(f'### {group_title}')
98
+ for doc_data in doc_data_list:
99
+ link = f'#{doc_data["template_type"]}-{doc_data["function_name"]}'.lower().replace(' ', '-')
100
+ output_lines.append(f'- [{doc_data["function_name"]}]({link})')
101
+
102
+ if not omit_toc:
103
+ output_lines.append('## Contents')
104
+ function_templates = [d for d in template_docs if d['template_type'] == 'Function']
105
+ add_toc_group(function_templates, 'Functions')
106
+ output_lines.append('')
107
+ process_templates = [d for d in template_docs if d['template_type'] == 'Process']
108
+ add_toc_group(process_templates, 'Processes')
109
+ output_lines.append('\n')
110
+
111
+
112
+ # Add template docs to output lines
113
+ for doc_data in template_docs:
114
+ output_lines.extend(doc_data['doc_lines'])
115
+ output_lines.append('')
116
+
117
+ with open(output_path, 'w') as f:
118
+ f.write('\n'.join(output_lines))
119
+
120
+ return Ok(None)
@@ -0,0 +1,49 @@
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 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
+ with open(output_path, 'w') as f:
46
+ f.write('\n'.join(template_codes))
47
+
48
+ print(f'Saved {len(template_codes)} template{"s" if len(template_codes) != 1 else ''} to "{output_path}".')
49
+ return Ok(None)
@@ -0,0 +1,54 @@
1
+ from typing import Literal
2
+ import re
3
+ from result import Result, Ok, Err
4
+ from dfpyre import Variable, Number, String, Text
5
+ from pyrecli.util import parse_templates_from_file
6
+
7
+
8
+ TEXT_CODE_PATTERNS = [
9
+ re.compile(r"%var\(([a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+)\)"),
10
+ re.compile(r"%index\(([a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+),\d+\)"),
11
+ re.compile(r"%entry\(([a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+),[a-zA-Z0-9!@#$%^&*~`\-_=+\\|;':\",.\/<>? ]+\)")
12
+ ]
13
+
14
+
15
+ def rename_var_in_text_code(s: str, var_to_rename: str, new_var_name: str):
16
+ for pattern in TEXT_CODE_PATTERNS:
17
+ match = pattern.search(s)
18
+ if match and match.group(1) == var_to_rename:
19
+ s = s.replace(match.group(1), new_var_name)
20
+ return s
21
+
22
+
23
+ def rename_command(input_path: str, output_path: str|None,
24
+ var_to_rename: str, new_var_name: str,
25
+ renamed_var_scope: Literal['game', 'saved', 'local', 'line']|None) -> Result[None, str]:
26
+ templates_result = parse_templates_from_file(input_path)
27
+ if templates_result.is_err():
28
+ return Err(templates_result.err_value)
29
+ templates = templates_result.ok_value
30
+
31
+ for template in templates:
32
+ for codeblock in template.codeblocks:
33
+ for argument in codeblock.args:
34
+ if isinstance(argument, Variable):
35
+ if argument.name == var_to_rename:
36
+ if renamed_var_scope is None:
37
+ argument.name = new_var_name
38
+ elif argument.scope != renamed_var_scope:
39
+ argument.name = new_var_name
40
+ argument.name = rename_var_in_text_code(argument.name, var_to_rename, new_var_name)
41
+
42
+ elif isinstance(argument, (Number, String, Text)) and isinstance(argument.value, str):
43
+ argument.value = rename_var_in_text_code(argument.value, var_to_rename, new_var_name)
44
+
45
+ if codeblock.type in {'call_func', 'start_process'}:
46
+ new_data = rename_var_in_text_code(codeblock.data.get('data'), var_to_rename, new_var_name)
47
+ codeblock.data['data'] = new_data
48
+
49
+ new_file_content = '\n'.join(t.build() for t in templates)
50
+ write_path = output_path if output_path else input_path
51
+ with open(write_path, 'w') as f:
52
+ f.write(new_file_content)
53
+
54
+ return Ok(None)
@@ -0,0 +1,24 @@
1
+ from result import Result, Ok, Err
2
+ from pyrecli.util import 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
+ with open(output_path, 'w') as f:
19
+ f.write(scan_results)
20
+
21
+ amount_templates = scan_results.count('\n')
22
+ print(f'Scanned {amount_templates} template{"s" if amount_templates != 1 else ''} successfully.')
23
+
24
+ return Ok(None)
@@ -0,0 +1,42 @@
1
+ import os
2
+ from result import Result, Ok, Err
3
+ from dfpyre import DFTemplate
4
+ from pyrecli.util import parse_templates_from_file
5
+
6
+
7
+ def write_to_directory(dir_name: str, templates: list[DFTemplate], flags: dict[str, int|bool]):
8
+ if not os.path.isdir(dir_name):
9
+ os.mkdir(dir_name)
10
+
11
+ for template in templates:
12
+ script_path = f'{dir_name}/{template._get_template_name()}.py'
13
+ script_string = template.generate_script(**flags)
14
+ with open(script_path, 'w') as f:
15
+ f.write(script_string)
16
+
17
+
18
+ def write_to_single_file(file_path: str, templates: list[DFTemplate], flags: dict[str, int|bool]):
19
+ file_content = []
20
+ for i, template in enumerate(templates):
21
+ if i == 0:
22
+ template_script = template.generate_script(include_import=True, assign_variable=True, **flags)
23
+ else:
24
+ template_script = template.generate_script(include_import=False, assign_variable=True, **flags)
25
+ file_content.append(template_script)
26
+
27
+ with open(file_path, 'w') as f:
28
+ f.write('\n\n'.join(file_content))
29
+
30
+
31
+ def script_command(input_path: str, output_path: str, one_file: bool, flags: dict[str, int|bool]) -> Result[None, str]:
32
+ templates_result = parse_templates_from_file(input_path)
33
+ if templates_result.is_err():
34
+ return Err(templates_result.err_value)
35
+ templates = templates_result.ok_value
36
+
37
+ if one_file:
38
+ write_to_single_file(output_path, templates, flags)
39
+ else:
40
+ write_to_directory(output_path, templates, flags)
41
+
42
+ return Ok(None)
@@ -0,0 +1,22 @@
1
+ from result import Result, Ok, Err
2
+ from pyrecli.util import connect_to_codeclient, parse_templates_from_file
3
+
4
+
5
+ def send_command(input_path: str) -> Result[None, str]:
6
+ templates_result = parse_templates_from_file(input_path)
7
+ if templates_result.is_err():
8
+ return Err(templates_result.err_value)
9
+ templates = templates_result.ok_value
10
+
11
+ ws_result = connect_to_codeclient()
12
+ if ws_result.is_err():
13
+ return Err(ws_result.err_value)
14
+ ws = ws_result.ok_value
15
+
16
+ for template in templates:
17
+ item = template.generate_template_item()
18
+ ws.send(f'give {item.get_snbt()}')
19
+
20
+ ws.close()
21
+ print(f'Sent {len(templates)} template{"s" if len(templates) != 1 else ''} successfully.')
22
+ return Ok(None)
@@ -0,0 +1,90 @@
1
+ import sys
2
+ import argparse
3
+
4
+ from pyrecli.command.scan import scan_command
5
+ from pyrecli.command.send import send_command
6
+ from pyrecli.command.script import script_command
7
+ from pyrecli.command.rename import rename_command
8
+ from pyrecli.command.grabinv import grabinv_command
9
+ from pyrecli.command.docs import docs_command
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(prog='pyrecli', description='Command line utilities for DiamondFire templates')
14
+ subparsers = parser.add_subparsers(dest='command', help='Available commands:', required=True, metavar='<command>')
15
+
16
+ parser_scan = subparsers.add_parser('scan', help='Scan the current plot templates with CodeClient')
17
+ parser_scan.add_argument('output_path', help='The file to output template data to', type=str)
18
+
19
+ parser_send = subparsers.add_parser('send', help='Send templates to DiamondFire with CodeClient')
20
+ parser_send.add_argument('input_path', help='The file containing template data', type=str)
21
+
22
+ parser_script = subparsers.add_parser('script', help='Create python scripts from template data')
23
+ parser_script.add_argument('input_path', help='The file containing template data', type=str)
24
+ parser_script.add_argument('output_path', help='The file or directory to output to', type=str)
25
+ parser_script.add_argument('--onefile', help='Output template data as a single script', action='store_true')
26
+ parser_script.add_argument('--indent_size', '-i', help='The multiple of spaces to add when indenting lines', type=int, default=4)
27
+ parser_script.add_argument('--literal_shorthand', '-ls', help='Output Text and Number items as strings and ints respectively', action='store_false')
28
+ parser_script.add_argument('--var_shorthand', '-vs', help='Write all variables using variable shorthand', action='store_true')
29
+ parser_script.add_argument('--preserve_slots', '-s', help='Save the positions of items within chests', action='store_true')
30
+ parser_script.add_argument('--build_and_send', '-b', help='Add `.build_and_send()` to the end of the generated template(s)', action='store_true')
31
+
32
+ parser_rename = subparsers.add_parser('rename', help='Rename a variable')
33
+ parser_rename.add_argument('input_path', help='The file containing template data', type=str)
34
+ parser_rename.add_argument('var_to_rename', help='The variable to rename', type=str)
35
+ parser_rename.add_argument('new_var_name', help='The new name for the variable', type=str)
36
+ parser_rename.add_argument('--var_to_rename_scope', '-s', help='The scope to match', type=str, default=None)
37
+ parser_rename.add_argument('--output_path', '-o', help='The file or directory to output to', type=str, default=None)
38
+
39
+ parser_grabinv = subparsers.add_parser('grabinv', help='Save all templates in the inventory to a file with CodeClient')
40
+ parser_grabinv.add_argument('output_path', help='The file to output template data to', type=str)
41
+
42
+ parser_docs = subparsers.add_parser('docs', help='Generate markdown documentation from template data')
43
+ parser_docs.add_argument('input_path', help='The file containing template data', type=str)
44
+ parser_docs.add_argument('output_path', help='The file or directory to output to', type=str)
45
+ parser_docs.add_argument('title', help='The title for the docs', type=str)
46
+ parser_docs.add_argument('--include_hidden', '-ih', help='Include hidden functions and processes', action='store_true')
47
+ parser_docs.add_argument('--notoc', help='Omit the table of contents', action='store_true')
48
+
49
+ parsed_args = parser.parse_args()
50
+
51
+ match parsed_args.command:
52
+ case 'scan':
53
+ command_result = scan_command(parsed_args.output_path)
54
+
55
+ case 'send':
56
+ command_result = send_command(parsed_args.input_path)
57
+
58
+ case 'script':
59
+ scriptgen_flags = {
60
+ 'indent_size': parsed_args.indent_size,
61
+ 'literal_shorthand': parsed_args.literal_shorthand,
62
+ 'var_shorthand': parsed_args.var_shorthand,
63
+ 'preserve_slots': parsed_args.preserve_slots,
64
+ 'build_and_send': parsed_args.build_and_send
65
+ }
66
+ command_result = script_command(parsed_args.input_path, parsed_args.output_path, parsed_args.onefile, scriptgen_flags)
67
+
68
+ case 'rename':
69
+ command_result = rename_command(
70
+ parsed_args.input_path, parsed_args.output_path,
71
+ parsed_args.var_to_rename, parsed_args.new_var_name, parsed_args.var_to_rename_scope
72
+ )
73
+
74
+ case 'grabinv':
75
+ command_result = grabinv_command(parsed_args.output_path)
76
+
77
+ case 'docs':
78
+ command_result = docs_command(
79
+ parsed_args.input_path, parsed_args.output_path,
80
+ parsed_args.title, parsed_args.include_hidden, parsed_args.notoc
81
+ )
82
+
83
+ if command_result.is_err():
84
+ print(command_result.err_value)
85
+ sys.exit(1)
86
+
87
+ sys.exit(0)
88
+
89
+ if __name__ == '__main__':
90
+ main()
@@ -0,0 +1,48 @@
1
+ import os
2
+ import re
3
+ from result import Result, Ok, Err
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
+
13
+ def connect_to_codeclient(scopes: str|None=None) -> Result[websocket.WebSocket, str]:
14
+ ws = websocket.WebSocket()
15
+ try:
16
+ ws.connect(CODECLIENT_URL)
17
+ except ConnectionRefusedError:
18
+ return Err('Failed to connect to CodeClient.')
19
+
20
+ print('Connected to CodeClient.')
21
+
22
+ if scopes:
23
+ print('Please run /auth in game.')
24
+ ws.send(f'scopes {scopes}')
25
+ auth_message = ws.recv()
26
+
27
+ if auth_message != 'auth':
28
+ return Err('Failed to authenticate.')
29
+ print('Authentication received.')
30
+
31
+ return Ok(ws)
32
+
33
+
34
+ def parse_templates_from_file(path: str) -> Result[list[DFTemplate], str]:
35
+ if not os.path.isfile(path):
36
+ return Err(f'"{path}" is not a file.')
37
+
38
+ with open(path, 'r') as f:
39
+ template_codes = f.read().split('\n')
40
+
41
+ for i, template_code in enumerate(template_codes):
42
+ if not BASE64_REGEX.match(template_code):
43
+ return Err(f'Template code at line {i+1} is not a base64 string.')
44
+
45
+ try:
46
+ return Ok([DFTemplate.from_code(c) for c in template_codes])
47
+ except Exception as e:
48
+ return Err(str(e))