FruitSalad 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ global-exclude *.ts
2
+ global-exclude *.tsbuildinfo
3
+ global-exclude *.map
4
+ prune src/FruitSalad/.vscode-extension/node_modules
5
+ prune src/FruitSalad/.vscode-extension/src
6
+ prune src/FruitSalad/.vscode-extension/out
7
+ recursive-exclude src/FruitSalad/.vscode-extension *.js
8
+ recursive-exclude src/FruitSalad/.vscode-extension *.d.ts
9
+ recursive-exclude src/FruitSalad/.vscode-extension package-lock.json
10
+ recursive-exclude src/FruitSalad/.vscode-extension tsconfig.json
11
+ include src/FruitSalad/.vscode-extension/package.json
12
+ include src/FruitSalad/.vscode-extension/icons/*
13
+ include src/FruitSalad/.vscode-extension/snippets/*
14
+ include src/FruitSalad/.vscode-extension/syntaxes/*
15
+ include src/FruitSalad/.vscode-extension/language-configuration.json
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: FruitSalad
3
+ Version: 0.1.0
4
+ Summary: A fruit salad project
5
+ Author: FruitSalad Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/your-username/FruitSalad
8
+ Project-URL: Repository, https://github.com/your-username/FruitSalad
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: blake3
21
+ Requires-Dist: pyyaml
22
+ Dynamic: license-file
23
+
24
+ # FruitSalad
25
+
26
+ A Python toolkit for working with `.cucumber` and `.melon` file formats, with VS Code extension support for syntax highlighting and file icons.
27
+
28
+ ## Features
29
+
30
+ - **Cucumber format** — Human-readable config files with BLAKE3 integrity verification
31
+ - **Melon format** — Binary serialization using TLV (Type-Length-Value) pattern with BLAKE3 checksums
32
+ - **Conversion** — Convert between `.cucumber`, `.melon`, `.json`, and `.yaml` formats
33
+ - **VS Code extension** — Syntax highlighting, snippets, and file icons for `.cucumber` and `.melon` files
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install FruitSalad
39
+ ```
40
+
41
+ ## CLI Usage
42
+
43
+ ```bash
44
+ # Convert between formats
45
+ fruitsalad convert input.cucumber output.json
46
+
47
+ # Install VS Code extension and configure file icons
48
+ fruitsalad vscode
49
+
50
+ # Convert specific formats
51
+ fruitsalad to-json input.cucumber
52
+ fruitsalad to-yaml input.cucumber
53
+ fruitsalad to-cucumber input.json
54
+ fruitsalad to-melon input.json
55
+ ```
56
+
57
+ ## Python API
58
+
59
+ ```python
60
+ from FruitSalad import load, save, reseal, test_file
61
+
62
+ # Load a .cucumber file
63
+ data = load("config.cucumber")
64
+ print(data)
65
+
66
+ # Save data
67
+ save("output.cucumber", data)
68
+
69
+ # Re-seal after manual edits
70
+ reseal("config.cucumber")
71
+
72
+ # Generate a test file
73
+ test_file("test.cucumber")
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,55 @@
1
+ # FruitSalad
2
+
3
+ A Python toolkit for working with `.cucumber` and `.melon` file formats, with VS Code extension support for syntax highlighting and file icons.
4
+
5
+ ## Features
6
+
7
+ - **Cucumber format** — Human-readable config files with BLAKE3 integrity verification
8
+ - **Melon format** — Binary serialization using TLV (Type-Length-Value) pattern with BLAKE3 checksums
9
+ - **Conversion** — Convert between `.cucumber`, `.melon`, `.json`, and `.yaml` formats
10
+ - **VS Code extension** — Syntax highlighting, snippets, and file icons for `.cucumber` and `.melon` files
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install FruitSalad
16
+ ```
17
+
18
+ ## CLI Usage
19
+
20
+ ```bash
21
+ # Convert between formats
22
+ fruitsalad convert input.cucumber output.json
23
+
24
+ # Install VS Code extension and configure file icons
25
+ fruitsalad vscode
26
+
27
+ # Convert specific formats
28
+ fruitsalad to-json input.cucumber
29
+ fruitsalad to-yaml input.cucumber
30
+ fruitsalad to-cucumber input.json
31
+ fruitsalad to-melon input.json
32
+ ```
33
+
34
+ ## Python API
35
+
36
+ ```python
37
+ from FruitSalad import load, save, reseal, test_file
38
+
39
+ # Load a .cucumber file
40
+ data = load("config.cucumber")
41
+ print(data)
42
+
43
+ # Save data
44
+ save("output.cucumber", data)
45
+
46
+ # Re-seal after manual edits
47
+ reseal("config.cucumber")
48
+
49
+ # Generate a test file
50
+ test_file("test.cucumber")
51
+ ```
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta:__legacy__"
4
+
5
+ [project]
6
+ name = "FruitSalad"
7
+ version = "0.1.0"
8
+ description = "A fruit salad project"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ {name = "FruitSalad Contributors"},
14
+ ]
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ "blake3",
18
+ "pyyaml",
19
+ ]
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Operating System :: OS Independent",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Topic :: Utilities",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/your-username/FruitSalad"
33
+ Repository = "https://github.com/your-username/FruitSalad"
34
+
35
+ [project.scripts]
36
+ fruitsalad = "FruitSalad.cli:main"
37
+
38
+ [tool.setuptools]
39
+ include-package-data = true
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ FruitSalad = [".vscode-extension/**"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <g transform="scale(1.7) translate(-21 -21)">
3
+ <circle cx="50" cy="50" r="40" fill="#388e3c" />
4
+ <circle cx="50" cy="50" r="32" fill="#a5d6a7" />
5
+
6
+ <g fill="#1b5e20" opacity="0.8">
7
+ <ellipse cx="50" cy="35" rx="4" ry="7" />
8
+ <ellipse cx="35" cy="50" rx="7" ry="4" />
9
+ <ellipse cx="65" cy="50" rx="7" ry="4" />
10
+ <ellipse cx="50" cy="65" rx="4" ry="7" />
11
+ </g>
12
+
13
+ <circle cx="50" cy="50" r="5" fill="#ffffff" opacity="0.15" />
14
+ </g>
15
+ </svg>
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="32 34 36 26">
2
+ <g transform="translate(50, 42) scale(1.7)">
3
+ <path d="M -18 -8 A 22 22 0 0 0 18 -8 L 0 18 Z" fill="#1b5e20"/>
4
+ <path d="M -15 -8 A 19 19 0 0 0 15 -8 L 0 15 Z" fill="#a5d6a7"/>
5
+ <path d="M -12 -8 A 16 16 0 0 0 12 -8 L 0 12 Z" fill="#e53935"/>
6
+ <circle cx="-4" cy="-1" r="1.8" fill="#1a1a1a"/>
7
+ <circle cx="4" cy="-1" r="1.8" fill="#1a1a1a"/>
8
+ <circle cx="0" cy="5" r="1.8" fill="#1a1a1a"/>
9
+ </g>
10
+ </svg>
@@ -0,0 +1,8 @@
1
+ {
2
+ "comments": {
3
+ "lineComment": "#"
4
+ },
5
+ "brackets": [],
6
+ "autoClosingPairs": [],
7
+ "surroundingPairs": []
8
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "FruitSalad",
3
+ "displayName": "FruitSalad",
4
+ "description": "Language support for .cucumber and .melon files — syntax highlighting, auto-complete, BLAKE3 integrity validation, and file icons.",
5
+ "version": "0.1.0",
6
+ "publisher": "fruit-salad",
7
+ "license": "MIT",
8
+ "engines": {
9
+ "vscode": "^1.125.0"
10
+ },
11
+ "categories": [
12
+ "Programming Languages",
13
+ "Snippets",
14
+ "Other"
15
+ ],
16
+ "activationEvents": [
17
+ "onLanguage:cucumber"
18
+ ],
19
+ "main": "./out/extension.js",
20
+ "contributes": {
21
+ "languages": [
22
+ {
23
+ "id": "cucumber",
24
+ "aliases": [
25
+ "Cucumber",
26
+ "cucumber"
27
+ ],
28
+ "extensions": [
29
+ ".cucumber"
30
+ ],
31
+ "icon": "./icons/cucumber.svg",
32
+ "configuration": "./language-configuration.json"
33
+ },
34
+ {
35
+ "id": "melon",
36
+ "aliases": [
37
+ "Melon",
38
+ "melon"
39
+ ],
40
+ "extensions": [
41
+ ".melon"
42
+ ],
43
+ "icon": "./icons/melon.svg"
44
+ }
45
+ ],
46
+ "grammars": [
47
+ {
48
+ "language": "cucumber",
49
+ "scopeName": "source.cucumber",
50
+ "path": "./syntaxes/cucumber.tmLanguage.json"
51
+ }
52
+ ],
53
+ "snippets": [
54
+ {
55
+ "language": "cucumber",
56
+ "path": "./snippets/cucumber.json"
57
+ }
58
+ ],
59
+ "commands": [
60
+ {
61
+ "command": "fruit-salad.verify",
62
+ "title": "FruitSalad: Verify hash"
63
+ },
64
+ {
65
+ "command": "fruit-salad.reseal",
66
+ "title": "FruitSalad: Reseal (update hash)"
67
+ }
68
+ ]
69
+ },
70
+ "devDependencies": {
71
+ "@types/node": "^26.0.1",
72
+ "@types/vscode": "^1.125.0",
73
+ "typescript": "^6.0.3"
74
+ }
75
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "Header block": {
3
+ "prefix": ["header", "hdr"],
4
+ "body": [
5
+ "--h-- Auto-Generated by FruitSalad --h--",
6
+ "--h-- BLAKE3: $1 --h--",
7
+ "$0"
8
+ ],
9
+ "description": "Insert the standard .cucumber header with BLAKE3 hash placeholder"
10
+ },
11
+ "Section block": {
12
+ "prefix": ["section", "sec"],
13
+ "body": [
14
+ "${1:section_name}",
15
+ "\t${2:key} | ${3:value}",
16
+ "$0"
17
+ ],
18
+ "description": "Insert a new section block with a key-value pair"
19
+ },
20
+ "Key-value pair": {
21
+ "prefix": ["kv", "keyvalue"],
22
+ "body": [
23
+ "${1:key} | ${2:value}",
24
+ "$0"
25
+ ],
26
+ "description": "Insert a key-value pair"
27
+ },
28
+ "Test metadata section": {
29
+ "prefix": ["test_meta", "metadata"],
30
+ "body": [
31
+ "test_meta",
32
+ "\tdate | $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE",
33
+ "\ttime | $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND",
34
+ "\tdevice | ${1:hostname}",
35
+ "$0"
36
+ ],
37
+ "description": "Insert a standard test_meta section"
38
+ },
39
+ "Server config section": {
40
+ "prefix": ["server", "servers"],
41
+ "body": [
42
+ "servers",
43
+ "\t${1:host} | ${2:10.0.0.1}",
44
+ "\t${3:port} | ${4:8080}",
45
+ "\t${5:ssl} | ${6:true}",
46
+ "$0"
47
+ ],
48
+ "description": "Insert a server configuration section"
49
+ },
50
+ "Database config section": {
51
+ "prefix": ["database", "db"],
52
+ "body": [
53
+ "database",
54
+ "\t${1:host} | ${2:localhost}",
55
+ "\t${3:port} | ${4:5432}",
56
+ "\t${5:name} | ${6:mydb}",
57
+ "$0"
58
+ ],
59
+ "description": "Insert a database configuration section"
60
+ }
61
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "scopeName": "source.cucumber",
3
+ "name": "Cucumber",
4
+ "fileTypes": [".cucumber"],
5
+ "patterns": [
6
+ { "include": "#header" },
7
+ { "include": "#comments" },
8
+ { "include": "#key-value" },
9
+ { "include": "#section" }
10
+ ],
11
+ "repository": {
12
+ "header": {
13
+ "match": "^--h--.*--h--$",
14
+ "name": "comment.block.header.cucumber"
15
+ },
16
+ "comments": {
17
+ "match": "^#.*$",
18
+ "name": "comment.line.number-sign.cucumber"
19
+ },
20
+ "section": {
21
+ "match": "^[a-zA-Z_][a-zA-Z0-9_]*$",
22
+ "name": "entity.name.section.cucumber"
23
+ },
24
+ "key-value": {
25
+ "begin": "^([ \\t]*)([a-zA-Z_][a-zA-Z0-9_ ]*)\\s*(\\|)",
26
+ "beginCaptures": {
27
+ "2": { "name": "variable.other.key.cucumber" },
28
+ "3": { "name": "punctuation.separator.key-value.cucumber" }
29
+ },
30
+ "end": "$",
31
+ "patterns": [
32
+ {
33
+ "match": "\\b(true|false)\\b",
34
+ "name": "constant.language.boolean.cucumber"
35
+ },
36
+ {
37
+ "match": "\\b\\d+(\\.\\d+)?\\b",
38
+ "name": "constant.numeric.cucumber"
39
+ }
40
+ ]
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,3 @@
1
+ from .cucumber import CucumberParser, load, save, reseal, test_file
2
+ from .melon import MelonSerializer
3
+ from .utils import convert, to_json, to_yaml, to_cucumber, to_melon
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,192 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+
8
+
9
+ def find_extension_dir():
10
+ pkg_dir = os.path.dirname(os.path.abspath(__file__))
11
+ ext_dir = os.path.join(pkg_dir, ".vscode-extension")
12
+ if os.path.isdir(ext_dir):
13
+ return ext_dir
14
+ return None
15
+
16
+
17
+ def find_vsix(ext_dir):
18
+ for f in os.listdir(ext_dir):
19
+ if f.endswith(".vsix"):
20
+ return os.path.join(ext_dir, f)
21
+ return None
22
+
23
+
24
+ def get_user_vscode_settings_path():
25
+ if sys.platform == "win32":
26
+ base = os.environ.get("APPDATA", os.path.join(os.path.expanduser("~"), "AppData", "Roaming"))
27
+ elif sys.platform == "darwin":
28
+ base = os.path.join(os.path.expanduser("~"), "Library", "Application Support")
29
+ else:
30
+ base = os.path.join(os.path.expanduser("~"), ".config")
31
+ return os.path.join(base, "Code", "User", "settings.json")
32
+
33
+
34
+ def install_icons(ext_dir):
35
+ """Copy icon SVGs to the VS Code extensions icons directory."""
36
+ icons_src = os.path.join(ext_dir, "icons")
37
+ if not os.path.isdir(icons_src):
38
+ print("Warning: icons directory not found in extension", file=sys.stderr)
39
+ return
40
+
41
+ # Target: C:\Users\User\.vscode\extensions\icons\
42
+ vscode_ext_icons = os.path.join(os.path.expanduser("~"), ".vscode", "extensions", "icons")
43
+ os.makedirs(vscode_ext_icons, exist_ok=True)
44
+
45
+ icon_mappings = {
46
+ "cucumber.svg": "fruit-cucumber.svg",
47
+ "melon.svg": "fruit-melon.svg",
48
+ }
49
+
50
+ for src_name, dst_name in icon_mappings.items():
51
+ src_path = os.path.join(icons_src, src_name)
52
+ dst_path = os.path.join(vscode_ext_icons, dst_name)
53
+ if os.path.exists(src_path):
54
+ shutil.copy2(src_path, dst_path)
55
+ print(f"Installed icon: {dst_path}")
56
+ else:
57
+ print(f"Warning: source icon not found: {src_path}", file=sys.stderr)
58
+
59
+
60
+ def update_user_settings():
61
+ """Append material-icon-theme.files.associations to the global VS Code user settings."""
62
+ settings_path = get_user_vscode_settings_path()
63
+ if not os.path.exists(settings_path):
64
+ print(f"Warning: user settings not found at {settings_path}", file=sys.stderr)
65
+ return
66
+
67
+ with open(settings_path, "r") as f:
68
+ settings = json.load(f)
69
+
70
+ settings.setdefault("material-icon-theme.files.associations", {}).update({
71
+ "*.cucumber": "../../icons/fruit-cucumber",
72
+ "*.melon": "../../icons/fruit-melon",
73
+ })
74
+
75
+ with open(settings_path, "w") as f:
76
+ json.dump(settings, f, indent=4)
77
+ f.write("\n")
78
+
79
+ print(f"Updated icon settings in global user settings: {settings_path}")
80
+
81
+
82
+ def cmd_vscode(args):
83
+ ext_dir = find_extension_dir()
84
+ if ext_dir is None:
85
+ print("Error: .vscode-extension directory not found", file=sys.stderr)
86
+ sys.exit(1)
87
+
88
+ vsix = find_vsix(ext_dir)
89
+ if vsix is None:
90
+ print("Error: No .vsix file found in .vscode-extension", file=sys.stderr)
91
+ sys.exit(1)
92
+
93
+ print(f"Installing VS Code extension: {vsix}")
94
+ try:
95
+ result = subprocess.run(
96
+ ["code", "--install-extension", vsix],
97
+ capture_output=True, text=True, timeout=30
98
+ )
99
+ if result.returncode == 0:
100
+ print(result.stdout.strip() or "Extension installed successfully")
101
+ else:
102
+ print(f"Warning: extension install may have failed:\n{result.stderr}", file=sys.stderr)
103
+ except FileNotFoundError:
104
+ print("Warning: 'code' command not found. Install VS Code and add it to PATH.", file=sys.stderr)
105
+ except subprocess.TimeoutExpired:
106
+ print("Warning: extension install timed out.", file=sys.stderr)
107
+
108
+ # Install icon files to the correct location
109
+ install_icons(ext_dir)
110
+
111
+ # Update global VS Code user settings
112
+ update_user_settings()
113
+
114
+ # Update workspace .vscode/settings.json
115
+ vscode_dir = os.path.join(os.getcwd(), ".vscode")
116
+ os.makedirs(vscode_dir, exist_ok=True)
117
+
118
+ settings_path = os.path.join(vscode_dir, "settings.json")
119
+ settings = {}
120
+ if os.path.exists(settings_path):
121
+ with open(settings_path, "r") as f:
122
+ settings = json.load(f)
123
+
124
+ settings.setdefault("files.associations", {}).update({
125
+ "*.cucumber": "cucumber",
126
+ "*.melon": "melon",
127
+ })
128
+ settings.setdefault("material-icon-theme.files.associations", {}).update({
129
+ "*.cucumber": "../../icons/fruit-cucumber",
130
+ "*.melon": "../../icons/fruit-melon",
131
+ })
132
+ settings.setdefault("material-icon-theme.languages.associations", {}).update({
133
+ "cucumber": "../../icons/fruit-cucumber",
134
+ "melon": "../../icons/fruit-melon",
135
+ })
136
+
137
+ with open(settings_path, "w") as f:
138
+ json.dump(settings, f, indent=2)
139
+ f.write("\n")
140
+
141
+ print(f"Updated icon settings in {settings_path}")
142
+
143
+
144
+ def main():
145
+ parser = argparse.ArgumentParser(prog="fruitsalad", description="FruitSalad CLI")
146
+ sub = parser.add_subparsers(dest="command")
147
+
148
+ vscode_parser = sub.add_parser("vscode", help="Install VS Code extension and set icons")
149
+ vscode_parser.set_defaults(func=cmd_vscode)
150
+
151
+ convert_parser = sub.add_parser("convert", help="Convert between file formats")
152
+ convert_parser.add_argument("in_path", help="Input file path")
153
+ convert_parser.add_argument("out_path", help="Output file path")
154
+ convert_parser.set_defaults(func=lambda a: from_utils("convert", a.in_path, a.out_path))
155
+
156
+ for name, help_text in [
157
+ ("to-json", "Convert to JSON"),
158
+ ("to-yaml", "Convert to YAML"),
159
+ ("to-cucumber", "Convert to .cucumber"),
160
+ ("to-melon", "Convert to .melon"),
161
+ ]:
162
+ p = sub.add_parser(name, help=help_text)
163
+ p.add_argument("in_path", help="Input file path")
164
+ p.add_argument("out_path", nargs="?", default=None, help="Output file path (optional)")
165
+ p.set_defaults(func=lambda a, _name=name.replace("-", "_"): from_utils(_name, a.in_path, a.out_path))
166
+
167
+ args = parser.parse_args()
168
+ if args.command is None:
169
+ parser.print_help()
170
+ sys.exit(1)
171
+ args.func(args)
172
+
173
+
174
+ def from_utils(func_name, in_path, out_path=None):
175
+ """Bridge to call utility functions from CLI."""
176
+ from .utils import convert, to_json, to_yaml, to_cucumber, to_melon
177
+ mapping = {
178
+ "convert": convert,
179
+ "to_json": to_json,
180
+ "to_yaml": to_yaml,
181
+ "to_cucumber": to_cucumber,
182
+ "to_melon": to_melon,
183
+ }
184
+ fn = mapping[func_name]
185
+ if out_path is not None:
186
+ fn(in_path, out_path)
187
+ else:
188
+ fn(in_path)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
@@ -0,0 +1,104 @@
1
+ import blake3
2
+ import os
3
+ import datetime
4
+ import platform
5
+
6
+ class CucumberParser:
7
+ def __init__(self):
8
+ self.data = {}
9
+
10
+ def _verify_hash(self, lines):
11
+ header_line = lines[1]
12
+ if "BLAKE3:" not in header_line:
13
+ raise ValueError("Invalid format: Missing BLAKE3 header line.")
14
+ expected_hash = header_line.split("BLAKE3:")[1].split("--h--")[0].strip()
15
+
16
+ content = "".join(lines[2:]).encode('utf-8')
17
+ actual_hash = blake3.blake3(content).hexdigest()
18
+
19
+ if actual_hash != expected_hash:
20
+ raise ValueError(f"Integrity Check Failed! Expected {expected_hash}, got {actual_hash}")
21
+
22
+ def _cast_value(self, val):
23
+ val = val.strip()
24
+ if val.lower() == 'true': return True
25
+ if val.lower() == 'false': return False
26
+ try:
27
+ if '.' in val: return float(val)
28
+ return int(val)
29
+ except ValueError:
30
+ return val
31
+
32
+ def load(self, filepath):
33
+ if not os.path.exists(filepath):
34
+ raise FileNotFoundError(f"Config file {filepath} not found.")
35
+
36
+ with open(filepath, 'r') as f:
37
+ lines = f.readlines()
38
+
39
+ self._verify_hash(lines)
40
+
41
+ current_section = self.data
42
+ for line in lines[2:]:
43
+ line = line.rstrip()
44
+ if not line or line.startswith('#'):
45
+ continue
46
+
47
+ if '|' not in line:
48
+ self.data[line.strip()] = {}
49
+ current_section = self.data[line.strip()]
50
+ else:
51
+ key, value = [p.strip() for p in line.split('|', 1)]
52
+ current_section[key] = self._cast_value(value)
53
+ return self.data
54
+
55
+ def save(self, filepath, data):
56
+ lines = ["--h-- Auto-Generated by FruitSalad --h--", "--h-- BLAKE3: PLACEHOLDER --h--"]
57
+
58
+ def format_dict(d, indent=""):
59
+ for k, v in d.items():
60
+ if isinstance(v, dict):
61
+ lines.append(f"{indent}{k}")
62
+ format_dict(v, indent + " ")
63
+ else:
64
+ lines.append(f"{indent}{k} | {v}")
65
+
66
+ format_dict(data)
67
+
68
+ content = "\n".join(lines[2:]) + "\n"
69
+ actual_hash = blake3.blake3(content.encode('utf-8')).hexdigest()
70
+ lines[1] = f"--h-- BLAKE3: {actual_hash} --h--"
71
+
72
+ with open(filepath, 'w') as f:
73
+ f.write("\n".join(lines) + "\n")
74
+
75
+ def reseal(self, filepath):
76
+ """Re-calculates the BLAKE3 hash for an existing .cucumber file after manual edits."""
77
+ with open(filepath, 'r') as f:
78
+ lines = f.readlines()
79
+
80
+ content = "".join(lines[2:]).encode('utf-8')
81
+ new_hash = blake3.blake3(content).hexdigest()
82
+
83
+ lines[1] = f"--h-- BLAKE3: {new_hash} --h--\n"
84
+ with open(filepath, 'w') as f:
85
+ f.writelines(lines)
86
+ print(f"Re-sealed {filepath} with hash: {new_hash}")
87
+
88
+ def test_file(self, filepath):
89
+ """Creates a diagnostic file with system metadata."""
90
+ test_data = {
91
+ "test_meta": {
92
+ "date": datetime.datetime.now().strftime("%Y-%m-%d"),
93
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
94
+ "device": platform.node()
95
+ }
96
+ }
97
+ self.save(filepath, test_data)
98
+ print(f"Test file generated at: {filepath}")
99
+
100
+ # Convenience functions
101
+ def load(filepath): return CucumberParser().load(filepath)
102
+ def save(filepath, data): CucumberParser().save(filepath, data)
103
+ def reseal(filepath): CucumberParser().reseal(filepath)
104
+ def test_file(filepath): CucumberParser().test_file(filepath)
@@ -0,0 +1,74 @@
1
+ import struct
2
+ import blake3
3
+ import os
4
+
5
+ # TLV Tag Definitions
6
+ TAG_NODE_ID = 0x01
7
+ TAG_NEIGHBOR = 0x02
8
+ TAG_KSIZE = 0x03
9
+
10
+ class MelonSerializer:
11
+ """
12
+ Handles binary serialization using TLV pattern:
13
+ [Magic:4][Version:1][Checksum:32][Payload:...]
14
+ """
15
+ MAGIC = b"MELN"
16
+ VERSION = 1
17
+
18
+ def save(self, filepath, data_dict):
19
+ """
20
+ Serializes a dictionary into a .melon file.
21
+ Example data_dict: {'node_id': b'...', 'ksize': 20}
22
+ """
23
+ payload = b""
24
+
25
+ # Packing simple types (Example logic)
26
+ if 'node_id' in data_dict:
27
+ val = data_dict['node_id']
28
+ payload += struct.pack(f"BB{len(val)}s", TAG_NODE_ID, len(val), val)
29
+
30
+ if 'ksize' in data_dict:
31
+ val = data_dict['ksize']
32
+ payload += struct.pack("BBB", TAG_KSIZE, 1, val)
33
+
34
+ # Integrity
35
+ checksum = blake3.blake3(payload).digest()
36
+ header = struct.pack(">4sB32s", self.MAGIC, self.VERSION, checksum)
37
+
38
+ with open(filepath, "wb") as f:
39
+ f.write(header + payload)
40
+
41
+ def load(self, filepath):
42
+ """Reads and deserializes a .melon file."""
43
+ with open(filepath, "rb") as f:
44
+ header = f.read(37)
45
+ magic, version, stored_checksum = struct.unpack(">4sB32s", header)
46
+
47
+ if magic != self.MAGIC:
48
+ raise ValueError("Not a valid .melon file")
49
+
50
+ payload = f.read()
51
+
52
+ # Verify Integrity
53
+ if blake3.blake3(payload).digest() != stored_checksum:
54
+ raise ValueError("Corrupted .melon file: Hash mismatch!")
55
+
56
+ # Parsing Payload (TLV loop)
57
+ data = {}
58
+ offset = 0
59
+ while offset < len(payload):
60
+ tag = payload[offset]
61
+ length = payload[offset + 1]
62
+ val = payload[offset + 2 : offset + 2 + length]
63
+
64
+ if tag == TAG_NODE_ID:
65
+ data['node_id'] = val
66
+ elif tag == TAG_KSIZE:
67
+ data['ksize'] = struct.unpack("B", val)[0]
68
+
69
+ offset += 2 + length
70
+ return data
71
+
72
+ # Convenience functions
73
+ def save(filepath, data): MelonSerializer().save(filepath, data)
74
+ def load(filepath): return MelonSerializer().load(filepath)
@@ -0,0 +1,90 @@
1
+ import json
2
+ import os
3
+ from .cucumber import CucumberParser
4
+ from .melon import MelonSerializer
5
+
6
+ try:
7
+ import yaml
8
+ HAS_YAML = True
9
+ except ImportError:
10
+ HAS_YAML = False
11
+
12
+
13
+ EXT_FORMAT_MAP = {
14
+ '.cucumber': 'cucumber',
15
+ '.melon': 'melon',
16
+ '.json': 'json',
17
+ '.yaml': 'yaml',
18
+ '.yml': 'yaml',
19
+ }
20
+
21
+
22
+ def read_data(filepath):
23
+ ext = os.path.splitext(filepath)[1].lower()
24
+ if ext == '.cucumber':
25
+ return CucumberParser().load(filepath)
26
+ elif ext == '.melon':
27
+ return MelonSerializer().load(filepath)
28
+ elif ext == '.json':
29
+ with open(filepath, 'r') as f:
30
+ return json.load(f)
31
+ elif ext in ('.yaml', '.yml'):
32
+ if not HAS_YAML:
33
+ raise ImportError("pyyaml is required to read YAML files")
34
+ with open(filepath, 'r') as f:
35
+ return yaml.safe_load(f)
36
+ else:
37
+ raise ValueError(f"Unknown format: {ext}")
38
+
39
+
40
+ def write_data(filepath, data):
41
+ ext = os.path.splitext(filepath)[1].lower()
42
+ if ext == '.cucumber':
43
+ CucumberParser().save(filepath, data)
44
+ elif ext == '.melon':
45
+ MelonSerializer().save(filepath, data)
46
+ elif ext == '.json':
47
+ with open(filepath, 'w') as f:
48
+ json.dump(data, f, indent=2)
49
+ elif ext in ('.yaml', '.yml'):
50
+ if not HAS_YAML:
51
+ raise ImportError("pyyaml is required to write YAML files")
52
+ with open(filepath, 'w') as f:
53
+ yaml.dump(data, f, default_flow_style=False)
54
+ else:
55
+ raise ValueError(f"Unknown format: {ext}")
56
+
57
+
58
+ def convert(in_path, out_path):
59
+ data = read_data(in_path)
60
+ write_data(out_path, data)
61
+ print(f"Converted {in_path} -> {out_path}")
62
+ return data
63
+
64
+
65
+ def to_json(in_path, out_path=None):
66
+ if out_path is None:
67
+ base, _ = os.path.splitext(in_path)
68
+ out_path = base + '.json'
69
+ return convert(in_path, out_path)
70
+
71
+
72
+ def to_yaml(in_path, out_path=None):
73
+ if out_path is None:
74
+ base, _ = os.path.splitext(in_path)
75
+ out_path = base + '.yaml'
76
+ return convert(in_path, out_path)
77
+
78
+
79
+ def to_cucumber(in_path, out_path=None):
80
+ if out_path is None:
81
+ base, _ = os.path.splitext(in_path)
82
+ out_path = base + '.cucumber'
83
+ return convert(in_path, out_path)
84
+
85
+
86
+ def to_melon(in_path, out_path=None):
87
+ if out_path is None:
88
+ base, _ = os.path.splitext(in_path)
89
+ out_path = base + '.melon'
90
+ return convert(in_path, out_path)
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: FruitSalad
3
+ Version: 0.1.0
4
+ Summary: A fruit salad project
5
+ Author: FruitSalad Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/your-username/FruitSalad
8
+ Project-URL: Repository, https://github.com/your-username/FruitSalad
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: blake3
21
+ Requires-Dist: pyyaml
22
+ Dynamic: license-file
23
+
24
+ # FruitSalad
25
+
26
+ A Python toolkit for working with `.cucumber` and `.melon` file formats, with VS Code extension support for syntax highlighting and file icons.
27
+
28
+ ## Features
29
+
30
+ - **Cucumber format** — Human-readable config files with BLAKE3 integrity verification
31
+ - **Melon format** — Binary serialization using TLV (Type-Length-Value) pattern with BLAKE3 checksums
32
+ - **Conversion** — Convert between `.cucumber`, `.melon`, `.json`, and `.yaml` formats
33
+ - **VS Code extension** — Syntax highlighting, snippets, and file icons for `.cucumber` and `.melon` files
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install FruitSalad
39
+ ```
40
+
41
+ ## CLI Usage
42
+
43
+ ```bash
44
+ # Convert between formats
45
+ fruitsalad convert input.cucumber output.json
46
+
47
+ # Install VS Code extension and configure file icons
48
+ fruitsalad vscode
49
+
50
+ # Convert specific formats
51
+ fruitsalad to-json input.cucumber
52
+ fruitsalad to-yaml input.cucumber
53
+ fruitsalad to-cucumber input.json
54
+ fruitsalad to-melon input.json
55
+ ```
56
+
57
+ ## Python API
58
+
59
+ ```python
60
+ from FruitSalad import load, save, reseal, test_file
61
+
62
+ # Load a .cucumber file
63
+ data = load("config.cucumber")
64
+ print(data)
65
+
66
+ # Save data
67
+ save("output.cucumber", data)
68
+
69
+ # Re-seal after manual edits
70
+ reseal("config.cucumber")
71
+
72
+ # Generate a test file
73
+ test_file("test.cucumber")
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,24 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/FruitSalad/__init__.py
6
+ src/FruitSalad/__main__.py
7
+ src/FruitSalad/cli.py
8
+ src/FruitSalad/cucumber.py
9
+ src/FruitSalad/melon.py
10
+ src/FruitSalad/utils.py
11
+ src/FruitSalad.egg-info/PKG-INFO
12
+ src/FruitSalad.egg-info/SOURCES.txt
13
+ src/FruitSalad.egg-info/dependency_links.txt
14
+ src/FruitSalad.egg-info/entry_points.txt
15
+ src/FruitSalad.egg-info/requires.txt
16
+ src/FruitSalad.egg-info/top_level.txt
17
+ src/FruitSalad/.vscode-extension/fruit-salad-0.1.0.vsix
18
+ src/FruitSalad/.vscode-extension/language-configuration.json
19
+ src/FruitSalad/.vscode-extension/package.json
20
+ src/FruitSalad/.vscode-extension/icons/cucumber.svg
21
+ src/FruitSalad/.vscode-extension/icons/melon.svg
22
+ src/FruitSalad/.vscode-extension/snippets/cucumber.json
23
+ src/FruitSalad/.vscode-extension/syntaxes/cucumber.tmLanguage.json
24
+ tests/test_parsing.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fruitsalad = FruitSalad.cli:main
@@ -0,0 +1,2 @@
1
+ blake3
2
+ pyyaml
@@ -0,0 +1 @@
1
+ FruitSalad
File without changes