Coverage for src/backend/InvenTree/InvenTree/config.py: 63%

293 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""Helper functions for loading InvenTree configuration options.""" 

2 

3import datetime 

4import json 

5import logging 

6import os 

7import random 

8import shutil 

9import string 

10import sys 

11from pathlib import Path 

12from typing import Any, Optional 

13 

14logger = logging.getLogger('inventree') 

15CONFIG_DATA = None 

16CONFIG_LOOKUPS = {} 

17 

18 

19def to_list(value, delimiter=','): 

20 """Take a configuration setting and make sure it is a list. 

21 

22 For example, we might have a configuration setting taken from the .config file, 

23 which is already a list. 

24 

25 However, the same setting may be specified via an environment variable, 

26 using a comma delimited string! 

27 """ 

28 if type(value) in [list, tuple]: 

29 return value 

30 

31 # Otherwise, force string value 

32 value = str(value) 

33 

34 return [x.strip() for x in value.split(delimiter)] 

35 

36 

37def to_dict(value): 

38 """Take a configuration setting and make sure it is a dict. 

39 

40 For example, we might have a configuration setting taken from the .config file, 

41 which is already an object/dict. 

42 

43 However, the same setting may be specified via an environment variable, 

44 using a valid JSON string! 

45 """ 

46 if value is None: 

47 return {} 

48 

49 if isinstance(value, dict): 49 ↛ 52line 49 didn't jump to line 52 because the condition on line 49 was always true

50 return value 

51 

52 try: 

53 return json.loads(value) 

54 except Exception as error: 

55 logger.exception( 

56 "Failed to parse value '%s' as JSON with error %s. Ensure value is a valid JSON string.", 

57 value, 

58 error, 

59 ) 

60 return {} 

61 

62 

63def is_true(x): 

64 """Shortcut function to determine if a value "looks" like a boolean.""" 

65 return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true', 'on'] 

66 

67 

68def get_base_dir() -> Path: 

69 """Returns the base (top-level) InvenTree directory.""" 

70 return Path(__file__).parent.parent.resolve() 

71 

72 

73def get_root_dir() -> Path: 

74 """Returns the InvenTree root directory.""" 

75 return get_base_dir().parent.parent.parent 

76 

77 

78def inventreeInstaller() -> Optional[str]: 

79 """Returns the installer for the running codebase - if set or detectable.""" 

80 load_version_file() 

81 

82 # First look in the environment variables, e.g. if running in docker 

83 installer = os.environ.get('INVENTREE_PKG_INSTALLER', '') 

84 

85 if installer: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true

86 return str(installer) 

87 

88 if is_true(os.environ.get('INVENTREE_DEVCONTAINER', 'False')): 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true

89 return 'DEV' 

90 

91 if is_true(os.environ.get('INVENTREE_DOCKER', 'False')): 91 ↛ 94line 91 didn't jump to line 94 because the condition on line 91 was always true

92 return 'DOC' 

93 

94 try: 

95 from django.conf import settings 

96 

97 from InvenTree.version import main_commit 

98 

99 if settings.DOCKER: 

100 return 'DOC' 

101 elif main_commit is not None: 

102 return 'GIT' 

103 except Exception: 

104 pass 

105 return None 

106 

107 

108def get_config_dir() -> Path: 

109 """Returns the InvenTree configuration directory depending on the install type.""" 

110 if inst := inventreeInstaller(): 110 ↛ 118line 110 didn't jump to line 118 because the condition on line 110 was always true

111 if inst == 'DOC': 111 ↛ 113line 111 didn't jump to line 113 because the condition on line 111 was always true

112 return Path('/home/inventree/data/').resolve() 

113 elif inst == 'DEV': 

114 return Path('/home/inventree/dev/').resolve() 

115 elif inst == 'PKG': 

116 return Path('/etc/inventree/').resolve() 

117 

118 return get_root_dir().joinpath('config').resolve() 

119 

120 

121def get_testfolder_dir() -> Path: 

122 """Returns the InvenTree test folder directory.""" 

123 return get_base_dir().joinpath('_testfolder').resolve() 

124 

125 

126def get_version_file() -> Path: 

127 """Returns the path of the InvenTree VERSION file. This does not ensure that the file exists.""" 

128 return get_root_dir().joinpath('VERSION').resolve() 

129 

130 

131def ensure_dir(path: Path, storage=None) -> None: 

132 """Ensure that a directory exists. 

133 

134 If it does not exist, create it. 

135 """ 

136 from django.core.files.base import ContentFile 

137 from django.core.files.storage import Storage 

138 

139 if storage and isinstance(storage, Storage): 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true

140 if not storage.exists(str(path)): 

141 storage.save(str(path / '.empty'), ContentFile('')) 

142 return 

143 

144 if not path.exists(): 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true

145 path.mkdir(parents=True, exist_ok=True) 

146 

147 

148def get_config_file(create=True) -> Path: 

149 """Returns the path of the InvenTree configuration file. 

150 

151 Note: It will be created it if does not already exist! 

152 """ 

153 conf_dir = get_config_dir() 

154 base_dir = get_base_dir() 

155 

156 cfg_filename = os.getenv('INVENTREE_CONFIG_FILE') 

157 

158 if cfg_filename: 158 ↛ 160line 158 didn't jump to line 160 because the condition on line 158 was always true

159 cfg_filename = Path(cfg_filename.strip()).resolve() 

160 elif get_base_dir().joinpath('config.yaml').exists(): 

161 # If the config file is in the old directory, use that 

162 cfg_filename = base_dir.joinpath('config.yaml').resolve() 

163 else: 

164 # Config file is *not* specified - use the default 

165 cfg_filename = conf_dir.joinpath('config.yaml').resolve() 

166 

167 if not cfg_filename.exists() and create: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

168 print( 

169 "InvenTree configuration file 'config.yaml' not found - creating default file" 

170 ) 

171 ensure_dir(cfg_filename.parent) 

172 

173 cfg_template = base_dir.joinpath('config_template.yaml') 

174 shutil.copyfile(cfg_template, cfg_filename) 

175 print(f'Created config file {cfg_filename}') 

176 

177 check_config_dir('INVENTREE_CONFIG_FILE', cfg_filename, conf_dir) 

178 return cfg_filename 

179 

180 

181def load_config_data(set_cache: bool = False) -> map | None: 

182 """Load configuration data from the config file. 

183 

184 Arguments: 

185 set_cache(bool): If True, the configuration data will be cached for future use after load. 

186 """ 

187 global CONFIG_DATA 

188 

189 # use cache if populated 

190 # skip cache if cache should be set 

191 if CONFIG_DATA is not None and not set_cache: 

192 return CONFIG_DATA 

193 

194 import yaml.parser 

195 

196 cfg_file = get_config_file() 

197 

198 with open(cfg_file, encoding='utf-8') as cfg: 

199 try: 

200 data = yaml.safe_load(cfg) 

201 except yaml.parser.ParserError as error: 

202 logger.error( 

203 "INVE-E13: Error reading InvenTree configuration file '%s': %s", 

204 cfg_file, 

205 error, 

206 ) 

207 sys.exit(1) 

208 

209 # Set the cache if requested 

210 if set_cache: 210 ↛ 213line 210 didn't jump to line 213 because the condition on line 210 was always true

211 CONFIG_DATA = data 

212 

213 return data 

214 

215 

216def do_typecast(value, type, var_name=None): 

217 """Attempt to typecast a value. 

218 

219 Arguments: 

220 value: Value to typecast 

221 type: Function to use for typecasting the value e.g. int, float, str, list, dict 

222 var_name: Name that should be logged e.g. 'INVENTREE_STATIC_ROOT'. Set if logging is required. 

223 

224 Returns: 

225 Typecast value or original value if typecasting failed. 

226 """ 

227 # Force 'list' of strings 

228 if type is list: 

229 value = to_list(value) 

230 

231 # Valid JSON string is required 

232 elif type is dict: 

233 value = to_dict(value) 

234 

235 # Special handling for boolean typecasting 

236 elif type is bool: 

237 val = is_true(value) 

238 return val 

239 

240 elif type is not None: 

241 # Try to typecast the value 

242 try: 

243 val = type(value) 

244 return val 

245 except Exception as error: 

246 if var_name: 

247 logger.exception( 

248 "Failed to typecast '%s' with value '%s' to type '%s' with error %s", 

249 var_name, 

250 value, 

251 type, 

252 error, 

253 ) 

254 return value 

255 

256 

257def get_config_value(config_key: str) -> Optional[Any]: 

258 """Helper function to retrieve a configuration value from the config file.""" 

259 cfg_data = load_config_data() 

260 

261 result = None 

262 

263 # Hack to allow 'path traversal' in configuration file 

264 for key in config_key.strip().split('.'): 

265 if type(cfg_data) is not dict or key not in cfg_data: 

266 result = None 

267 break 

268 

269 result = cfg_data[key] 

270 cfg_data = cfg_data[key] 

271 

272 return result 

273 

274 

275def get_setting(env_var=None, config_key=None, default_value=None, typecast=None): 

276 """Helper function for retrieving a configuration setting value. 

277 

278 - First preference is to look for the environment variable 

279 - Second preference is to look for the value of the settings file 

280 - Third preference is the default value 

281 

282 Arguments: 

283 env_var: Name of the environment variable e.g. 'INVENTREE_STATIC_ROOT' 

284 config_key: Key to lookup in the configuration file 

285 default_value: Value to return if first two options are not provided 

286 typecast: Function to use for typecasting the value e.g. int, float, str, list, dict 

287 """ 

288 

289 def set_metadata(source: str): 

290 """Set lookup metadata for the setting.""" 

291 global CONFIG_LOOKUPS 

292 

293 key = env_var or config_key 

294 CONFIG_LOOKUPS[key] = { 

295 'env_var': env_var, 

296 'config_key': config_key, 

297 'default_value': default_value, 

298 'source': source, 

299 'accessed': datetime.datetime.now(), 

300 } 

301 

302 # First, try to load from the environment variables 

303 if env_var is not None: 303 ↛ 311line 303 didn't jump to line 311 because the condition on line 303 was always true

304 val = os.getenv(env_var, None) 

305 

306 if val is not None: 

307 set_metadata('env') 

308 return do_typecast(val, typecast, var_name=env_var) 

309 

310 # Next, try to load from configuration file 

311 if config_key is not None: 

312 result = get_config_value(config_key) 

313 

314 if result is not None: 

315 set_metadata('yaml') 

316 return do_typecast(result, typecast, var_name=env_var) 

317 

318 # Finally, return the default value 

319 set_metadata('default') 

320 return do_typecast(default_value, typecast, var_name=env_var) 

321 

322 

323def get_boolean_setting(env_var=None, config_key=None, default_value=False): 

324 """Helper function for retrieving a boolean configuration setting.""" 

325 return is_true(get_setting(env_var, config_key, default_value)) 

326 

327 

328def get_media_dir(create=True, error=True): 

329 """Return the absolute path for the 'media' directory (where uploaded files are stored).""" 

330 md = get_setting('INVENTREE_MEDIA_ROOT', 'media_root') 

331 

332 if not md: 332 ↛ 333line 332 didn't jump to line 333 because the condition on line 332 was never true

333 if error: 

334 raise FileNotFoundError('INVENTREE_MEDIA_ROOT not specified') 

335 else: 

336 return None 

337 

338 md = Path(md).resolve() 

339 

340 if create: 340 ↛ 343line 340 didn't jump to line 343 because the condition on line 340 was always true

341 md.mkdir(parents=True, exist_ok=True) 

342 

343 return md 

344 

345 

346def get_static_dir(create=True, error=True): 

347 """Return the absolute path for the 'static' directory (where static files are stored).""" 

348 sd = get_setting('INVENTREE_STATIC_ROOT', 'static_root') 

349 

350 if not sd: 350 ↛ 351line 350 didn't jump to line 351 because the condition on line 350 was never true

351 if error: 

352 raise FileNotFoundError('INVENTREE_STATIC_ROOT not specified') 

353 else: 

354 return None 

355 

356 sd = Path(sd).resolve() 

357 

358 if create: 358 ↛ 361line 358 didn't jump to line 361 because the condition on line 358 was always true

359 sd.mkdir(parents=True, exist_ok=True) 

360 

361 return sd 

362 

363 

364def get_backup_dir(create=True, error=True): 

365 """Return the absolute path for the backup directory.""" 

366 bd = get_setting('INVENTREE_BACKUP_DIR', 'backup_dir') 

367 

368 if not bd: 368 ↛ 369line 368 didn't jump to line 369 because the condition on line 368 was never true

369 if error: 

370 raise FileNotFoundError('INVENTREE_BACKUP_DIR not specified') 

371 else: 

372 return None 

373 

374 bd = Path(bd).resolve() 

375 

376 if create: 376 ↛ 379line 376 didn't jump to line 379 because the condition on line 376 was always true

377 bd.mkdir(parents=True, exist_ok=True) 

378 

379 return bd 

380 

381 

382def get_plugin_file() -> Path: 

383 """Returns the path of the InvenTree plugins specification file. 

384 

385 Note: It will be created if it does not already exist! 

386 """ 

387 # Check if the plugin.txt file (specifying required plugins) is specified 

388 plugin_file = get_setting('INVENTREE_PLUGIN_FILE', 'plugin_file') 

389 

390 if not plugin_file: 390 ↛ 392line 390 didn't jump to line 392 because the condition on line 390 was never true

391 # If not specified, look in the same directory as the configuration file 

392 config_dir = get_config_file().parent 

393 plugin_file = config_dir.joinpath('plugins.txt') 

394 else: 

395 # Make sure we are using a modern Path object 

396 plugin_file = Path(plugin_file) 

397 

398 if not plugin_file.exists(): 398 ↛ 410line 398 didn't jump to line 410 because the condition on line 398 was always true

399 logger.warning( 

400 'Plugin configuration file does not exist - creating default file' 

401 ) 

402 logger.info("Creating plugin file at '%s'", plugin_file) 

403 ensure_dir(plugin_file.parent) 

404 

405 # If opening the file fails (no write permission, for example), then this will throw an error 

406 plugin_file.write_text( 

407 '# InvenTree Plugins (uses PIP framework to install)\n\n' 

408 ) 

409 

410 check_config_dir('INVENTREE_PLUGIN_FILE', plugin_file) 

411 return plugin_file 

412 

413 

414def get_plugin_dir(): 

415 """Returns the path of the custom plugins directory.""" 

416 return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir') 

417 

418 

419def get_secret_key(return_path: bool = False) -> str | Path: 

420 """Return the secret key value which will be used by django. 

421 

422 Following options are tested, in descending order of preference: 

423 

424 A) Check for environment variable INVENTREE_SECRET_KEY => Use raw key data 

425 B) Check for environment variable INVENTREE_SECRET_KEY_FILE => Load key data from file 

426 C) Look for default key file "secret_key.txt" 

427 D) Create "secret_key.txt" if it does not exist 

428 

429 Args: 

430 return_path (bool): If True, return the path to the secret key file instead of the key data. 

431 """ 

432 # Look for environment variable 

433 if secret_key := get_setting('INVENTREE_SECRET_KEY', 'secret_key'): 433 ↛ 434line 433 didn't jump to line 434 because the condition on line 433 was never true

434 logger.info('SECRET_KEY loaded by INVENTREE_SECRET_KEY') # pragma: no cover 

435 return str(secret_key) 

436 

437 # Look for secret key file 

438 if secret_key_file := get_setting('INVENTREE_SECRET_KEY_FILE', 'secret_key_file'): 438 ↛ 440line 438 didn't jump to line 440 because the condition on line 438 was always true

439 secret_key_file = Path(secret_key_file).resolve() 

440 elif get_base_dir().joinpath('secret_key.txt').exists(): 

441 secret_key_file = get_base_dir().joinpath('secret_key.txt') 

442 else: 

443 # Default location for secret key file 

444 secret_key_file = get_config_dir().joinpath('secret_key.txt').resolve() 

445 check_config_dir('INVENTREE_SECRET_KEY_FILE', secret_key_file) 

446 

447 if not secret_key_file.exists(): 447 ↛ 456line 447 didn't jump to line 456 because the condition on line 447 was always true

448 logger.info("Generating random key file at '%s'", secret_key_file) 

449 ensure_dir(secret_key_file.parent) 

450 

451 # Create a random key file 

452 options = string.digits + string.ascii_letters + string.punctuation 

453 key = ''.join([random.choice(options) for _idx in range(100)]) 

454 secret_key_file.write_text(key) 

455 

456 if return_path: 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true

457 return secret_key_file 

458 

459 logger.debug("Loading SECRET_KEY from '%s'", secret_key_file) 

460 return secret_key_file.read_text().strip() 

461 

462 

463def get_oidc_private_key(return_path: bool = False) -> str | Path: 

464 """Return the private key for OIDC authentication. 

465 

466 Following options are tested, in descending order of preference: 

467 A) Check for environment variable INVENTREE_OIDC_PRIVATE_KEY or config yalue => Use raw key data 

468 B) Check for environment variable INVENTREE_OIDC_PRIVATE_KEY_FILE or config value => Load key data from file 

469 C) Create "oidc.pem" if it does not exist 

470 """ 

471 RSA_KEY = get_setting('INVENTREE_OIDC_PRIVATE_KEY', 'oidc_private_key') 

472 if RSA_KEY: 472 ↛ 473line 472 didn't jump to line 473 because the condition on line 472 was never true

473 logger.info('RSA_KEY loaded by INVENTREE_OIDC_PRIVATE_KEY') # pragma: no cover 

474 return RSA_KEY 

475 

476 # Look for private key file 

477 key_loc = Path( 

478 get_setting( 

479 'INVENTREE_OIDC_PRIVATE_KEY_FILE', 

480 'oidc_private_key_file', 

481 get_config_dir().joinpath('oidc.pem'), 

482 ) 

483 ) 

484 

485 # Trying old default location 

486 if not key_loc.exists(): 486 ↛ 491line 486 didn't jump to line 491 because the condition on line 486 was always true

487 old_def_path = get_base_dir().joinpath('oidc.pem') 

488 if old_def_path.exists(): 488 ↛ 489line 488 didn't jump to line 489 because the condition on line 488 was never true

489 key_loc = old_def_path.resolve() 

490 

491 check_config_dir('INVENTREE_OIDC_PRIVATE_KEY_FILE', key_loc) 

492 if key_loc.exists(): 492 ↛ 493line 492 didn't jump to line 493 because the condition on line 492 was never true

493 return key_loc.read_text() if not return_path else key_loc 

494 else: 

495 from cryptography.hazmat.primitives import serialization 

496 from cryptography.hazmat.primitives.asymmetric import rsa 

497 

498 # Default location for private key file 

499 logger.info("Generating oidc key file at '%s'", key_loc) 

500 ensure_dir(key_loc.parent) 

501 

502 # Create a random key file 

503 new_key = rsa.generate_private_key(public_exponent=65537, key_size=4096) 

504 # Write our key to disk for safe keeping 

505 with open(str(key_loc), 'wb') as f: 

506 f.write( 

507 new_key.private_bytes( 

508 encoding=serialization.Encoding.PEM, 

509 format=serialization.PrivateFormat.TraditionalOpenSSL, 

510 encryption_algorithm=serialization.NoEncryption(), 

511 ) 

512 ) 

513 return key_loc.read_text() if not return_path else key_loc 

514 

515 

516def get_custom_file( 

517 env_ref: str, conf_ref: str, log_ref: str, lookup_media: bool = False 

518): 

519 """Returns the checked path to a custom file. 

520 

521 Set lookup_media to True to also search in the media folder. 

522 """ 

523 from django.contrib.staticfiles.storage import StaticFilesStorage 

524 from django.core.files.storage import default_storage 

525 

526 value = get_setting(env_ref, conf_ref, None) 

527 

528 if not value: 

529 return None 

530 

531 static_storage = StaticFilesStorage() 

532 

533 if static_storage.exists(value): 

534 logger.info('Loading %s from %s directory: %s', log_ref, 'static', value) 

535 elif lookup_media and default_storage.exists(value): 

536 logger.info('Loading %s from %s directory: %s', log_ref, 'media', value) 

537 else: 

538 add_dir_str = ' or media' if lookup_media else '' 

539 logger.warning( 

540 "The %s file '%s' could not be found in the static %s directories", 

541 log_ref, 

542 value, 

543 add_dir_str, 

544 ) 

545 value = False 

546 

547 return value 

548 

549 

550def get_frontend_settings(debug=True): 

551 """Return a dictionary of settings for the frontend interface.""" 

552 # New settings 

553 frontend_settings = get_setting( 

554 'INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict 

555 ) 

556 

557 base_url = get_setting( 

558 'INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'web', typecast=str 

559 ) 

560 

561 # Set the base URL for the user interface 

562 # This is the UI path e.g. '/web/' 

563 if 'base_url' not in frontend_settings: 563 ↛ 567line 563 didn't jump to line 567 because the condition on line 563 was always true

564 frontend_settings['base_url'] = base_url 

565 

566 # If provided, specify the API host 

567 api_host = frontend_settings.get('api_host', None) or get_setting( 

568 'INVENTREE_FRONTEND_API_HOST', 'frontend_api_host', None 

569 ) 

570 

571 if api_host: 571 ↛ 572line 571 didn't jump to line 572 because the condition on line 571 was never true

572 frontend_settings['api_host'] = api_host 

573 

574 # Set the server list 

575 frontend_settings['server_list'] = frontend_settings.get('server_list', []) 

576 

577 # Set the debug flag 

578 frontend_settings['debug'] = debug 

579 

580 if 'environment' not in frontend_settings: 580 ↛ 583line 580 didn't jump to line 583 because the condition on line 580 was always true

581 frontend_settings['environment'] = 'development' if debug else 'production' 

582 

583 if (debug and 'show_server_selector' not in frontend_settings) or len( 583 ↛ 591line 583 didn't jump to line 591 because the condition on line 583 was always true

584 frontend_settings['server_list'] 

585 ) == 0: 

586 # In debug mode, show server selector by default 

587 # If no servers are specified, show server selector 

588 frontend_settings['show_server_selector'] = True 

589 

590 # Support compatibility with "legacy" URLs? 

591 try: 

592 frontend_settings['url_compatibility'] = bool( 

593 frontend_settings.get('url_compatibility', True) 

594 ) 

595 except Exception: 

596 # If the value is not a boolean, set it to True 

597 frontend_settings['url_compatibility'] = True 

598 

599 return frontend_settings 

600 

601 

602def check_config_dir( 

603 setting_name: str, current_path: Path, config_dir: Optional[Path] = None 

604) -> None: 

605 """Warn if the config directory is not used.""" 

606 if not config_dir: 

607 config_dir = get_config_dir() 

608 

609 if not current_path.is_relative_to(config_dir): 609 ↛ 610line 609 didn't jump to line 610 because the condition on line 609 was never true

610 logger.warning( 

611 "INVE-W10 - Config for '%s' not in recommended directory '%s'.", 

612 setting_name, 

613 config_dir, 

614 ) 

615 

616 try: 

617 from common.settings import GlobalWarningCode, set_global_warning 

618 

619 set_global_warning( 

620 GlobalWarningCode.UNCOMMON_CONFIG, {'path': str(config_dir)} 

621 ) 

622 except ModuleNotFoundError: # pragma: no cover 

623 pass 

624 

625 return 

626 

627 

628VERSION_LOADED = False 

629"""Flag to indicate if the VERSION file has been loaded in this process.""" 

630 

631 

632def load_version_file(): 

633 """Load the VERSION file if it exists and place the contents into the general execution environment. 

634 

635 Returns: 

636 True if the VERSION file was loaded (now or previously), False otherwise. 

637 """ 

638 global VERSION_LOADED 

639 if VERSION_LOADED: 639 ↛ 640line 639 didn't jump to line 640 because the condition on line 639 was never true

640 return True 

641 

642 # Load the VERSION file if it exists 

643 from dotenv import load_dotenv 

644 

645 version_file = get_version_file() 

646 if version_file.exists(): 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true

647 load_dotenv(version_file) 

648 VERSION_LOADED = True 

649 return True 

650 return False