Spaces:
Sleeping
Sleeping
File size: 12,861 Bytes
216c0a4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | import os
import re
import json
import importlib.util
import random
import threading
# Regular expression to extract requirements JSON from template files
REQUIREMENTS_PATTERN = re.compile(r'REQUIREMENTS_BEGIN\s*({.*?})\s*REQUIREMENTS_END', re.DOTALL)
# Dictionary to store template mappings
templates = {
'echarts_py': {}, # chart_type -> module
'echarts-js': {}, # chart_type -> js_file_path
'd3-js': {}, # chart_type -> js_file_path
'vegalite_py': {} # chart_type -> module
}
# 全局标识符,用于跟踪是否已扫描过模板
_templates_scanned = False
_templates_lock = threading.RLock()
def _safe_registry_name(value):
value = value.lower()
return re.sub(r'[^a-z0-9_]+', '_', value).strip('_')
def _duplicate_alias_for_path(chart_name, item_path, engine_dir, chart_dict):
stem = _safe_registry_name(os.path.splitext(os.path.basename(item_path))[0])
parent = _safe_registry_name(os.path.basename(os.path.dirname(item_path)))
rel = _safe_registry_name(os.path.splitext(os.path.relpath(item_path, engine_dir))[0])
candidates = [
stem,
f"{chart_name}__{parent}",
f"{chart_name}__{rel}",
]
for candidate in candidates:
if candidate and candidate not in chart_dict:
return candidate
index = 2
while f"{chart_name}__duplicate_{index}" in chart_dict:
index += 1
return f"{chart_name}__duplicate_{index}"
def _register_chart_template(chart_dict, chart_name, template_info, engine_dir):
"""Register one chart name while preserving overwritten entries as aliases."""
if chart_name in chart_dict:
existing = chart_dict[chart_name]
alias = _duplicate_alias_for_path(chart_name, existing['template'], engine_dir, chart_dict)
existing_alias = dict(existing)
existing_alias['registry_alias'] = alias
existing_alias['alias_for_chart_name'] = chart_name
chart_dict[alias] = existing_alias
chart_dict[chart_name] = template_info
def load_python_template(file_path):
"""Load a Python template module from a file path"""
module_name = os.path.basename(file_path).replace('.py', '')
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def extract_requirements(file_path):
"""Extract requirements JSON from a template file"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find requirements section
match = REQUIREMENTS_PATTERN.search(content)
if match:
try:
requirements = json.loads(match.group(1))
return requirements
except json.JSONDecodeError:
print(f"Warning: Invalid JSON in requirements section of {file_path}")
return None
def scan_directory(dir_path, engine_type, file_extension):
"""
递归扫描目录及其子目录,寻找符合条件的模板文件
Args:
dir_path: 要扫描的目录路径
engine_type: 引擎类型,'echarts_py', 'echarts-js' 或 'd3-js'
file_extension: 文件扩展名,'.py' 或 '.js'
"""
if not os.path.exists(dir_path):
return
# 遍历目录中的所有文件和子目录
for item in os.listdir(dir_path):
item_path = os.path.join(dir_path, item)
# 如果是目录,递归扫描
if os.path.isdir(item_path):
scan_directory(item_path, engine_type, file_extension)
# 如果是符合条件的文件
elif os.path.isfile(item_path) and item.endswith(file_extension):
# 对于Python文件,跳过以__开头的文件
if file_extension == '.py' and item.startswith('__'):
continue
# 提取需求并注册模板
requirements = extract_requirements(item_path)
# if engine_type == 'vegalite_py':
# print(f"requirements: {requirements['chart_name']}")
if requirements and 'chart_type' in requirements:
chart_type = requirements['chart_type'].lower()
# 获取chart_name,如果没有则使用文件名
chart_name = requirements.get('chart_name', os.path.basename(item_path).split('.')[0]).lower()
# 如果该chart_type还不存在,初始化一个空字典
if chart_type not in templates[engine_type]:
templates[engine_type][chart_type] = {}
# 根据引擎类型处理不同的模板
if engine_type == 'echarts_py':
template = load_python_template(item_path)
else: # echarts-js 或 d3-js
template = item_path
template_info = {
'engine_type': engine_type,
'template': template,
'requirements': requirements,
'source_chart_name': chart_name,
}
chart_dict = templates[engine_type][chart_type]
engine_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), engine_type)
# 存储模板信息为 [engine, template]
_register_chart_template(chart_dict, chart_name, template_info, engine_dir)
for alias_name in requirements.get('chart_aliases', []):
alias_name = str(alias_name).lower()
if not alias_name or alias_name == chart_name:
continue
alias_info = dict(template_info)
alias_info['registry_alias'] = alias_name
alias_info['alias_for_chart_name'] = chart_name
_register_chart_template(chart_dict, alias_name, alias_info, engine_dir)
# 计算相对于模板引擎主目录的路径
template_dir = os.path.dirname(os.path.abspath(__file__))
engine_dir = os.path.join(template_dir, engine_type)
rel_path = os.path.relpath(item_path, engine_dir)
# print(f"Registered {engine_type} template: {chart_type} -> {chart_name} -> {rel_path}")
def scan_templates(force=False):
"""
扫描模板目录并构建映射
Args:
force: 如果为True,即使已经扫描过也会强制重新扫描
"""
global _templates_scanned
with _templates_lock:
# 如果已经扫描过且不强制重新扫描,则直接返回
if _templates_scanned and not force:
return templates
# 清空现有模板
templates['vegalite_py'].clear()
templates['echarts_py'].clear()
templates['echarts-js'].clear()
templates['d3-js'].clear()
template_dir = os.path.dirname(os.path.abspath(__file__))
# 扫描 echarts_py 目录及子目录
echarts_py_dir = os.path.join(template_dir, 'echarts_py')
scan_directory(echarts_py_dir, 'echarts_py', '.py')
# 扫描 echarts-js 目录及子目录
echarts_js_dir = os.path.join(template_dir, 'echarts-js')
scan_directory(echarts_js_dir, 'echarts-js', '.js')
# 扫描 d3-js 目录及子目录
d3_js_dir = os.path.join(template_dir, 'd3-js')
scan_directory(d3_js_dir, 'd3-js', '.js')
# 扫描 vegalite_py 目录及子目录
vegalite_py_dir = os.path.join(template_dir, 'vegalite_py')
scan_directory(vegalite_py_dir, 'vegalite_py', '.py')
# 标记已完成扫描
_templates_scanned = True
return templates
def get_template_for_chart_type(chart_type, engine_preference=None):
"""
Get the best template for a given chart type
Args:
chart_type: The chart type to look for
engine_preference: Optional list of engine preferences ['echarts_py', 'echarts-js', 'd3-js']
in the order of preference
Returns:
tuple of (engine, template) where template is either a module or file path
"""
global _templates_scanned
# 如果尚未扫描模板,先扫描
if not _templates_scanned:
scan_templates()
chart_type = chart_type.lower()
if engine_preference is None:
engine_preference = ['echarts_py', 'echarts-js', 'd3-js']
# Try each engine in order of preference
for engine in engine_preference:
if chart_type in templates[engine]:
# 如果存在多个chart_name的template,随机返回一个
chart_names = list(templates[engine][chart_type].keys())
if chart_names:
selected_name = random.choice(chart_names)
template_info = templates[engine][chart_type][selected_name]
return template_info['engine_type'], template_info['template']
# Try partial matches
for engine in engine_preference:
for template_type in templates[engine]:
if chart_type in template_type or template_type in chart_type:
# 随机选择一个chart_name
chart_names = list(templates[engine][template_type].keys())
if chart_names:
selected_name = random.choice(chart_names)
template_info = templates[engine][template_type][selected_name]
return template_info['engine_type'], template_info['template']
return None, None
def get_template_for_chart_name(chart_name, engine_preference=None):
"""
Get the template for a specific chart name
Args:
chart_name: The chart name to look for
engine_preference: Optional list of engine preferences ['echarts_py', 'echarts-js', 'd3-js']
in the order of preference
Returns:
tuple of (engine, template) where template is either a module or file path
"""
global _templates_scanned
# 如果尚未扫描模板,先扫描
if not _templates_scanned:
scan_templates()
chart_name = chart_name.lower()
# Try each engine in order of preference
for engine in templates:
for chart_type, chart_dict in templates[engine].items():
if chart_name in chart_dict:
template_info = chart_dict[chart_name]
return template_info['engine_type'], template_info['template']
# Try partial matches
# for engine in engine_preference:
# for chart_type, chart_dict in templates[engine].items():
# for name in chart_dict:
# if chart_name in name or name in chart_name:
# return chart_dict[name]
# 找到重叠最多的匹配
best_match = None
max_overlap = 0
best_result = None
# print("chart_dict:", templates[engine])
for engine in templates:
for chart_type, chart_dict in templates[engine].items():
for name in chart_dict:
# 计算两个字符串的重叠长度
overlap = len(set(chart_name) & set(name))
if overlap > max_overlap:
#print("overlap:", overlap)
max_overlap = overlap
best_match = name
#print("best_match:", best_match)
template_info = chart_dict[name]
best_result = (template_info['engine_type'], template_info['template'])
#print("best_result:", best_result)
if best_result:
return best_result
return None, None
def get_template_for_template_key(template_key):
"""
Get the template for an exact registry key in the form
"<engine>/<chart_type>/<chart_name>".
"""
global _templates_scanned
if not _templates_scanned:
scan_templates()
try:
engine, chart_type, chart_name = template_key.split("/", 2)
except ValueError:
return None, None
chart_type = chart_type.lower()
chart_name = chart_name.lower()
template_info = templates.get(engine, {}).get(chart_type, {}).get(chart_name)
if not template_info:
return None, None
return template_info["engine_type"], template_info["template"]
# 在主模块运行时,扫描模板并打印信息
if __name__ == '__main__':
scan_templates()
print("\nAvailable templates:")
for engine, templates_dict in templates.items():
print(f"\n{engine}:")
for chart_type, chart_names_dict in templates_dict.items():
print(f" - {chart_type}:")
for chart_name in chart_names_dict:
print(f" * {chart_name}")
|