Skip to content

Commit 3cf00a3

Browse files
committed
DEV: add command to handle adding new command pages
1 parent 9ed6b79 commit 3cf00a3

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed

build/add_cmds.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import json
4+
import logging
5+
import os
6+
import sys
7+
8+
from components.syntax import Command
9+
from components.markdown import Markdown
10+
11+
12+
def command_filename(name: str) -> str:
13+
"""Convert command name to filename format."""
14+
return name.lower().replace(' ', '-')
15+
16+
17+
def parse_args() -> argparse.Namespace:
18+
parser = argparse.ArgumentParser(description='Creates new Redis command pages from JSON input')
19+
parser.add_argument('json_file', type=str,
20+
help='Path to JSON file containing command definitions')
21+
parser.add_argument('--loglevel', type=str,
22+
default='INFO',
23+
help='Python logging level (overwrites LOGLEVEL env var)')
24+
return parser.parse_args()
25+
26+
27+
def validate_json_structure(data: dict, filename: str) -> None:
28+
"""Validate that the JSON has the expected structure for Redis commands."""
29+
if not isinstance(data, dict):
30+
raise ValueError(f"JSON file {filename} must contain a dictionary at root level")
31+
32+
for command_name, command_data in data.items():
33+
if not isinstance(command_data, dict):
34+
raise ValueError(f"Command '{command_name}' must be a dictionary")
35+
36+
# Check for required fields
37+
required_fields = ['summary', 'since', 'group']
38+
for field in required_fields:
39+
if field not in command_data:
40+
logging.warning(f"Command '{command_name}' missing recommended field: {field}")
41+
42+
# Validate arguments structure if present
43+
if 'arguments' in command_data:
44+
if not isinstance(command_data['arguments'], list):
45+
raise ValueError(f"Command '{command_name}' arguments must be a list")
46+
47+
48+
def load_and_validate_json(filepath: str) -> dict:
49+
"""Load and validate the JSON file containing command definitions."""
50+
if not os.path.exists(filepath):
51+
raise FileNotFoundError(f"JSON file not found: {filepath}")
52+
53+
try:
54+
with open(filepath, 'r') as f:
55+
data = json.load(f)
56+
except json.JSONDecodeError as e:
57+
raise ValueError(f"Invalid JSON in file {filepath}: {e}")
58+
59+
validate_json_structure(data, filepath)
60+
return data
61+
62+
63+
def add_standard_categories(fm_data: dict) -> None:
64+
"""Add the standard categories from create.sh script."""
65+
standard_categories = [
66+
'docs', 'develop', 'stack', 'oss', 'rs', 'rc', 'oss', 'kubernetes', 'clients'
67+
]
68+
fm_data['categories'] = standard_categories
69+
70+
71+
def get_full_command_name(command_name: str, command_data: dict) -> str:
72+
"""Get the full command name, handling container commands."""
73+
container = command_data.get('container')
74+
if container:
75+
return f"{container} {command_name}"
76+
return command_name
77+
78+
79+
def generate_command_frontmatter(command_name: str, command_data: dict, all_commands: dict) -> dict:
80+
"""Generate complete Hugo frontmatter for a command using existing build infrastructure."""
81+
# Get the full command name (handles container commands)
82+
full_command_name = get_full_command_name(command_name, command_data)
83+
84+
# Create Command object to generate syntax using the full command name
85+
c = Command(full_command_name, command_data)
86+
87+
# Start with the command data
88+
fm_data = command_data.copy()
89+
90+
# Add required Hugo frontmatter fields
91+
fm_data.update({
92+
'title': full_command_name,
93+
'linkTitle': full_command_name,
94+
'description': command_data.get('summary'),
95+
'syntax_str': str(c),
96+
'syntax_fmt': c.syntax(),
97+
'hidden': False # Default to not hidden
98+
})
99+
100+
# Add the standard categories from create.sh
101+
add_standard_categories(fm_data)
102+
103+
return fm_data
104+
105+
106+
def generate_argument_sections(command_data: dict) -> str:
107+
"""Generate placeholder sections for Required arguments and Optional arguments."""
108+
content = ""
109+
110+
arguments = command_data.get('arguments', [])
111+
if not arguments:
112+
return content
113+
114+
required_args = []
115+
optional_args = []
116+
117+
# Categorize arguments
118+
for arg in arguments:
119+
if arg.get('optional', False):
120+
optional_args.append(arg)
121+
else:
122+
required_args.append(arg)
123+
124+
# Generate Required arguments section
125+
if required_args:
126+
content += "## Required arguments\n\n"
127+
for arg in required_args:
128+
arg_name = arg.get('name', 'unknown')
129+
arg_type = arg.get('type', 'unknown')
130+
display_text = arg.get('display_text', arg_name)
131+
132+
content += f"<details open><summary><code>{display_text}</code></summary>\n\n"
133+
content += f"TODO: Add description for {arg_name} ({arg_type})\n\n"
134+
content += "</details>\n\n"
135+
136+
# Generate Optional arguments section
137+
if optional_args:
138+
content += "## Optional arguments\n\n"
139+
for arg in optional_args:
140+
arg_name = arg.get('name', 'unknown')
141+
arg_type = arg.get('type', 'unknown')
142+
display_text = arg.get('display_text', arg_name)
143+
token = arg.get('token', '')
144+
145+
content += f"<details open><summary><code>{token if token else display_text}</code></summary>\n\n"
146+
content += f"TODO: Add description for {arg_name} ({arg_type})\n\n"
147+
content += "</details>\n\n"
148+
149+
return content
150+
151+
152+
def generate_return_section() -> str:
153+
"""Generate placeholder Return information section."""
154+
return '''## Return information
155+
156+
{{< multitabs id="return-info"
157+
tab1="RESP2"
158+
tab2="RESP3" >}}
159+
160+
TODO: Add RESP2 return information
161+
162+
-tab-sep-
163+
164+
TODO: Add RESP3 return information
165+
166+
{{< /multitabs >}}
167+
168+
'''
169+
170+
171+
def generate_complete_markdown_content(command_name: str, command_data: dict) -> str:
172+
"""Generate the complete markdown content for a command page."""
173+
content = ""
174+
175+
# Add command summary as the main description
176+
summary = command_data.get('summary', f'TODO: Add summary for {command_name}')
177+
content += f"{summary}\n\n"
178+
179+
# Add argument sections
180+
content += generate_argument_sections(command_data)
181+
182+
# Add return information section
183+
content += generate_return_section()
184+
185+
return content
186+
187+
188+
def create_command_file(command_name: str, command_data: dict, all_commands: dict) -> str:
189+
"""Create a complete command markdown file with frontmatter and content."""
190+
# Get the full command name (handles container commands)
191+
full_command_name = get_full_command_name(command_name, command_data)
192+
193+
# Generate the file path using the full command name
194+
filename = command_filename(full_command_name)
195+
filepath = f'content/commands/{filename}.md'
196+
197+
# Ensure the directory exists
198+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
199+
200+
# Check if file already exists
201+
if os.path.exists(filepath):
202+
logging.warning(f"File {filepath} already exists, skipping...")
203+
return filepath
204+
205+
# Generate frontmatter
206+
frontmatter_data = generate_command_frontmatter(command_name, command_data, all_commands)
207+
208+
# Generate content
209+
content = generate_complete_markdown_content(command_name, command_data)
210+
211+
# Create markdown object and set data
212+
md = Markdown(filepath)
213+
md.fm_data = frontmatter_data
214+
md.payload = content
215+
216+
# Write the file
217+
md.persist()
218+
219+
logging.info(f"Created command file: {filepath}")
220+
return filepath
221+
222+
223+
if __name__ == '__main__':
224+
args = parse_args()
225+
226+
# Configure logging BEFORE creating objects
227+
log_level = getattr(logging, args.loglevel.upper())
228+
logging.basicConfig(
229+
level=log_level,
230+
format='%(message)s %(filename)s:%(lineno)d - %(funcName)s',
231+
force=True # Force reconfiguration in case logging was already configured
232+
)
233+
234+
try:
235+
# Load and validate JSON data
236+
commands_data = load_and_validate_json(args.json_file)
237+
logging.info(f"Loaded {len(commands_data)} commands from {args.json_file}")
238+
239+
# Process each command and generate markdown files
240+
created_files = []
241+
for command_name in commands_data:
242+
try:
243+
logging.info(f"Processing command: {command_name}")
244+
filepath = create_command_file(command_name, commands_data[command_name], commands_data)
245+
created_files.append(filepath)
246+
except Exception as e:
247+
logging.error(f"Failed to create file for command '{command_name}': {e}")
248+
# Continue processing other commands
249+
continue
250+
251+
# Summary
252+
logging.info(f"Successfully created {len(created_files)} command files:")
253+
for filepath in created_files:
254+
logging.info(f" - {filepath}")
255+
256+
except (FileNotFoundError, ValueError) as e:
257+
logging.error(f"Error: {e}")
258+
sys.exit(1)
259+
except Exception as e:
260+
logging.error(f"Unexpected error: {e}")
261+
sys.exit(1)

0 commit comments

Comments
 (0)