refactor: re-layput project
- move assets (raw jsons, icons, meshes and i18n data) into the root of repo. - move script into the root of repo. - modify blender manifest according to this changes. - optimize gitignore.
This commit is contained in:
		
							
								
								
									
										207
									
								
								scripts/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								scripts/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,207 @@
 | 
			
		||||
# Byte-compiled / optimized / DLL files
 | 
			
		||||
__pycache__/
 | 
			
		||||
*.py[codz]
 | 
			
		||||
*$py.class
 | 
			
		||||
 | 
			
		||||
# C extensions
 | 
			
		||||
*.so
 | 
			
		||||
 | 
			
		||||
# Distribution / packaging
 | 
			
		||||
.Python
 | 
			
		||||
build/
 | 
			
		||||
develop-eggs/
 | 
			
		||||
dist/
 | 
			
		||||
downloads/
 | 
			
		||||
eggs/
 | 
			
		||||
.eggs/
 | 
			
		||||
lib/
 | 
			
		||||
lib64/
 | 
			
		||||
parts/
 | 
			
		||||
sdist/
 | 
			
		||||
var/
 | 
			
		||||
wheels/
 | 
			
		||||
share/python-wheels/
 | 
			
		||||
*.egg-info/
 | 
			
		||||
.installed.cfg
 | 
			
		||||
*.egg
 | 
			
		||||
MANIFEST
 | 
			
		||||
 | 
			
		||||
# PyInstaller
 | 
			
		||||
#  Usually these files are written by a python script from a template
 | 
			
		||||
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
 | 
			
		||||
*.manifest
 | 
			
		||||
*.spec
 | 
			
		||||
 | 
			
		||||
# Installer logs
 | 
			
		||||
pip-log.txt
 | 
			
		||||
pip-delete-this-directory.txt
 | 
			
		||||
 | 
			
		||||
# Unit test / coverage reports
 | 
			
		||||
htmlcov/
 | 
			
		||||
.tox/
 | 
			
		||||
.nox/
 | 
			
		||||
.coverage
 | 
			
		||||
.coverage.*
 | 
			
		||||
.cache
 | 
			
		||||
nosetests.xml
 | 
			
		||||
coverage.xml
 | 
			
		||||
*.cover
 | 
			
		||||
*.py.cover
 | 
			
		||||
.hypothesis/
 | 
			
		||||
.pytest_cache/
 | 
			
		||||
cover/
 | 
			
		||||
 | 
			
		||||
# Translations
 | 
			
		||||
*.mo
 | 
			
		||||
*.pot
 | 
			
		||||
 | 
			
		||||
# Django stuff:
 | 
			
		||||
*.log
 | 
			
		||||
local_settings.py
 | 
			
		||||
db.sqlite3
 | 
			
		||||
db.sqlite3-journal
 | 
			
		||||
 | 
			
		||||
# Flask stuff:
 | 
			
		||||
instance/
 | 
			
		||||
.webassets-cache
 | 
			
		||||
 | 
			
		||||
# Scrapy stuff:
 | 
			
		||||
.scrapy
 | 
			
		||||
 | 
			
		||||
# Sphinx documentation
 | 
			
		||||
docs/_build/
 | 
			
		||||
 | 
			
		||||
# PyBuilder
 | 
			
		||||
.pybuilder/
 | 
			
		||||
target/
 | 
			
		||||
 | 
			
		||||
# Jupyter Notebook
 | 
			
		||||
.ipynb_checkpoints
 | 
			
		||||
 | 
			
		||||
# IPython
 | 
			
		||||
profile_default/
 | 
			
		||||
ipython_config.py
 | 
			
		||||
 | 
			
		||||
# pyenv
 | 
			
		||||
#   For a library or package, you might want to ignore these files since the code is
 | 
			
		||||
#   intended to run in multiple environments; otherwise, check them in:
 | 
			
		||||
# .python-version
 | 
			
		||||
 | 
			
		||||
# pipenv
 | 
			
		||||
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 | 
			
		||||
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
 | 
			
		||||
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
 | 
			
		||||
#   install all needed dependencies.
 | 
			
		||||
#Pipfile.lock
 | 
			
		||||
 | 
			
		||||
# UV
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 | 
			
		||||
#   This is especially recommended for binary packages to ensure reproducibility, and is more
 | 
			
		||||
#   commonly ignored for libraries.
 | 
			
		||||
#uv.lock
 | 
			
		||||
 | 
			
		||||
# poetry
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
 | 
			
		||||
#   This is especially recommended for binary packages to ensure reproducibility, and is more
 | 
			
		||||
#   commonly ignored for libraries.
 | 
			
		||||
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
 | 
			
		||||
#poetry.lock
 | 
			
		||||
#poetry.toml
 | 
			
		||||
 | 
			
		||||
# pdm
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
 | 
			
		||||
#   pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
 | 
			
		||||
#   https://pdm-project.org/en/latest/usage/project/#working-with-version-control
 | 
			
		||||
#pdm.lock
 | 
			
		||||
#pdm.toml
 | 
			
		||||
.pdm-python
 | 
			
		||||
.pdm-build/
 | 
			
		||||
 | 
			
		||||
# pixi
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
 | 
			
		||||
#pixi.lock
 | 
			
		||||
#   Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
 | 
			
		||||
#   in the .venv directory. It is recommended not to include this directory in version control.
 | 
			
		||||
.pixi
 | 
			
		||||
 | 
			
		||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 | 
			
		||||
__pypackages__/
 | 
			
		||||
 | 
			
		||||
# Celery stuff
 | 
			
		||||
celerybeat-schedule
 | 
			
		||||
celerybeat.pid
 | 
			
		||||
 | 
			
		||||
# SageMath parsed files
 | 
			
		||||
*.sage.py
 | 
			
		||||
 | 
			
		||||
# Environments
 | 
			
		||||
.env
 | 
			
		||||
.envrc
 | 
			
		||||
.venv
 | 
			
		||||
env/
 | 
			
		||||
venv/
 | 
			
		||||
ENV/
 | 
			
		||||
env.bak/
 | 
			
		||||
venv.bak/
 | 
			
		||||
 | 
			
		||||
# Spyder project settings
 | 
			
		||||
.spyderproject
 | 
			
		||||
.spyproject
 | 
			
		||||
 | 
			
		||||
# Rope project settings
 | 
			
		||||
.ropeproject
 | 
			
		||||
 | 
			
		||||
# mkdocs documentation
 | 
			
		||||
/site
 | 
			
		||||
 | 
			
		||||
# mypy
 | 
			
		||||
.mypy_cache/
 | 
			
		||||
.dmypy.json
 | 
			
		||||
dmypy.json
 | 
			
		||||
 | 
			
		||||
# Pyre type checker
 | 
			
		||||
.pyre/
 | 
			
		||||
 | 
			
		||||
# pytype static type analyzer
 | 
			
		||||
.pytype/
 | 
			
		||||
 | 
			
		||||
# Cython debug symbols
 | 
			
		||||
cython_debug/
 | 
			
		||||
 | 
			
		||||
# PyCharm
 | 
			
		||||
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
 | 
			
		||||
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 | 
			
		||||
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
 | 
			
		||||
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 | 
			
		||||
#.idea/
 | 
			
		||||
 | 
			
		||||
# Abstra
 | 
			
		||||
# Abstra is an AI-powered process automation framework.
 | 
			
		||||
# Ignore directories containing user credentials, local state, and settings.
 | 
			
		||||
# Learn more at https://abstra.io/docs
 | 
			
		||||
.abstra/
 | 
			
		||||
 | 
			
		||||
# Visual Studio Code
 | 
			
		||||
#  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 
 | 
			
		||||
#  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
 | 
			
		||||
#  and can be added to the global gitignore or merged into this file. However, if you prefer, 
 | 
			
		||||
#  you could uncomment the following to ignore the entire vscode folder
 | 
			
		||||
# .vscode/
 | 
			
		||||
 | 
			
		||||
# Ruff stuff:
 | 
			
		||||
.ruff_cache/
 | 
			
		||||
 | 
			
		||||
# PyPI configuration file
 | 
			
		||||
.pypirc
 | 
			
		||||
 | 
			
		||||
# Cursor
 | 
			
		||||
#  Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
 | 
			
		||||
#  exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
 | 
			
		||||
#  refer to https://docs.cursor.com/context/ignore-files
 | 
			
		||||
.cursorignore
 | 
			
		||||
.cursorindexingignore
 | 
			
		||||
 | 
			
		||||
# Marimo
 | 
			
		||||
marimo/_static/
 | 
			
		||||
marimo/_lsp/
 | 
			
		||||
__marimo__/
 | 
			
		||||
							
								
								
									
										3
									
								
								scripts/.style.yapf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								scripts/.style.yapf
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
[style]
 | 
			
		||||
column_limit=120
 | 
			
		||||
i18n_function_call=tr
 | 
			
		||||
							
								
								
									
										27
									
								
								scripts/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								scripts/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
# Tools
 | 
			
		||||
 | 
			
		||||
These tool scripts is served for BBP_NG specifically. We use Astral UV with a single Python project file to manage these tools. You can browse their usage in this file.
 | 
			
		||||
 | 
			
		||||
## Build Icons
 | 
			
		||||
 | 
			
		||||
Build essential icons used by BBP_NG in Blender.
 | 
			
		||||
 | 
			
		||||
Execute `uv run build_icons.py`
 | 
			
		||||
 | 
			
		||||
## Build JSONs
 | 
			
		||||
 | 
			
		||||
Compress BME prototype JSON files into smaller size.
 | 
			
		||||
 | 
			
		||||
Execute `uv run build_json.py`
 | 
			
		||||
 | 
			
		||||
## Validate BME Prototype
 | 
			
		||||
 | 
			
		||||
Validate the correction of BME prorotype JSON files.
 | 
			
		||||
 | 
			
		||||
Execute `uv run validate_json.py`
 | 
			
		||||
 | 
			
		||||
## Extract BME Translation
 | 
			
		||||
 | 
			
		||||
Extract the translation template from BME prorotype JSON files.
 | 
			
		||||
 | 
			
		||||
Execute `uv run extract_json.py`
 | 
			
		||||
							
								
								
									
										3
									
								
								scripts/bme.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								scripts/bme.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
import enum
 | 
			
		||||
from typing import Optional, Self
 | 
			
		||||
from pydantic import BaseModel, RootModel, Field, model_validator, ValidationError
 | 
			
		||||
							
								
								
									
										433
									
								
								scripts/bme_relatives.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										433
									
								
								scripts/bme_relatives.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,433 @@
 | 
			
		||||
import typing
 | 
			
		||||
import simple_po, bme_utils
 | 
			
		||||
 | 
			
		||||
#region Translation Constant
 | 
			
		||||
 | 
			
		||||
## TODO:
 | 
			
		||||
#  This translation context string prefix is cpoied from UTIL_translation.py.
 | 
			
		||||
#  If the context string of translation changed, please synchronize it.
 | 
			
		||||
 | 
			
		||||
CTX_TRANSLATION: str = 'BBP/BME'
 | 
			
		||||
 | 
			
		||||
#endregion
 | 
			
		||||
 | 
			
		||||
#region BME Tokens
 | 
			
		||||
 | 
			
		||||
## TODO:
 | 
			
		||||
#  These token are copied from UTIL_bme.py.
 | 
			
		||||
#  If anything changed, such as BME standard, these tokens should be synchronized between these 2 modules.
 | 
			
		||||
 | 
			
		||||
TOKEN_IDENTIFIER: str = 'identifier'
 | 
			
		||||
 | 
			
		||||
TOKEN_SHOWCASE: str = 'showcase'
 | 
			
		||||
TOKEN_SHOWCASE_TITLE: str = 'title'
 | 
			
		||||
TOKEN_SHOWCASE_ICON: str = 'icon'
 | 
			
		||||
TOKEN_SHOWCASE_TYPE: str = 'type'
 | 
			
		||||
TOKEN_SHOWCASE_CFGS: str = 'cfgs'
 | 
			
		||||
TOKEN_SHOWCASE_CFGS_FIELD: str = 'field'
 | 
			
		||||
TOKEN_SHOWCASE_CFGS_TYPE: str = 'type'
 | 
			
		||||
TOKEN_SHOWCASE_CFGS_TITLE: str = 'title'
 | 
			
		||||
TOKEN_SHOWCASE_CFGS_DESC: str = 'desc'
 | 
			
		||||
TOKEN_SHOWCASE_CFGS_DEFAULT: str = 'default'
 | 
			
		||||
 | 
			
		||||
TOKEN_SKIP: str = 'skip'
 | 
			
		||||
 | 
			
		||||
TOKEN_PARAMS: str = 'params'
 | 
			
		||||
TOKEN_PARAMS_FIELD: str = 'field'
 | 
			
		||||
TOKEN_PARAMS_DATA: str = 'data'
 | 
			
		||||
 | 
			
		||||
TOKEN_VARS: str = 'vars'
 | 
			
		||||
TOKEN_VARS_FIELD: str = 'field'
 | 
			
		||||
TOKEN_VARS_DATA: str = 'data'
 | 
			
		||||
 | 
			
		||||
TOKEN_VERTICES: str = 'vertices'
 | 
			
		||||
TOKEN_VERTICES_SKIP: str = 'skip'
 | 
			
		||||
TOKEN_VERTICES_DATA: str = 'data'
 | 
			
		||||
 | 
			
		||||
TOKEN_FACES: str = 'faces'
 | 
			
		||||
TOKEN_FACES_SKIP: str = 'skip'
 | 
			
		||||
TOKEN_FACES_TEXTURE: str = 'texture'
 | 
			
		||||
TOKEN_FACES_INDICES: str = 'indices'
 | 
			
		||||
TOKEN_FACES_UVS: str = 'uvs'
 | 
			
		||||
TOKEN_FACES_NORMALS: str = 'normals'
 | 
			
		||||
 | 
			
		||||
TOKEN_INSTANCES: str = 'instances'
 | 
			
		||||
TOKEN_INSTANCES_IDENTIFIER: str = 'identifier'
 | 
			
		||||
TOKEN_INSTANCES_SKIP: str = 'skip'
 | 
			
		||||
TOKEN_INSTANCES_PARAMS: str = 'params'
 | 
			
		||||
TOKEN_INSTANCES_TRANSFORM: str = 'transform'
 | 
			
		||||
 | 
			
		||||
#endregion
 | 
			
		||||
 | 
			
		||||
# TODO: finish BME validator
 | 
			
		||||
 | 
			
		||||
# class ReporterWithHierarchy():
 | 
			
		||||
#     """
 | 
			
		||||
#     BME validator and extractor specifically used reporter
 | 
			
		||||
#     which auotmatically use hierarchy as its context when outputing.
 | 
			
		||||
#     """
 | 
			
		||||
 | 
			
		||||
#     __mReporter: bme_utils.Reporter
 | 
			
		||||
#     __mHierarchy: bme_utils.Hierarchy
 | 
			
		||||
 | 
			
		||||
#     def __init__(self, reporter: bme_utils.Reporter, hierarchy: bme_utils.Hierarchy):
 | 
			
		||||
#         self.__mReporter = reporter
 | 
			
		||||
#         self.__mHierarchy = hierarchy
 | 
			
		||||
 | 
			
		||||
#     def error(self, msg: str) -> None:
 | 
			
		||||
#         self.__mReporter.error(msg, self.__mHierarchy.build_hierarchy_string())
 | 
			
		||||
#     def warning(self, msg: str) -> None:
 | 
			
		||||
#         self.__mReporter.warning(msg, self.__mHierarchy.build_hierarchy_string())
 | 
			
		||||
#     def info(self, msg: str) -> None:
 | 
			
		||||
#         self.__mReporter.info(msg, self.__mHierarchy.build_hierarchy_string())
 | 
			
		||||
 | 
			
		||||
# class UniqueField():
 | 
			
		||||
#     """
 | 
			
		||||
#     Some BME prototype fields should be unique in globl scope.
 | 
			
		||||
#     So BME validator should check this. That's the feature this class provided.
 | 
			
		||||
 | 
			
		||||
#     This class is an abstract class and should not be used directly.
 | 
			
		||||
#     Use child class please.
 | 
			
		||||
#     """
 | 
			
		||||
 | 
			
		||||
#     __mUniques: set[str]
 | 
			
		||||
#     __mReporter: ReporterWithHierarchy
 | 
			
		||||
 | 
			
		||||
#     def __init__(self, reporter: ReporterWithHierarchy):
 | 
			
		||||
#         self.__mUniques = set()
 | 
			
		||||
#         self.__mReporter = reporter
 | 
			
		||||
 | 
			
		||||
#     def register(self, entry: str) -> bool:
 | 
			
		||||
#         """
 | 
			
		||||
#         @brief Try to register given entry in unique.
 | 
			
		||||
#         @details
 | 
			
		||||
#         If given entry is not presented in unique set, given entry will be inserted and return True.
 | 
			
		||||
#         If given entry is already available in unique set, this function will use reporter to output an error message and return False.
 | 
			
		||||
#         @param[in] entry The entry to be checked and inserted.
 | 
			
		||||
#         @return True if entry is unique, otherwise false.
 | 
			
		||||
#         """
 | 
			
		||||
#         if entry in self.__mUniques:
 | 
			
		||||
#             self.__mReporter.error(self._get_error_msg(entry))
 | 
			
		||||
#             return False
 | 
			
		||||
#         else:
 | 
			
		||||
#             self.__mUniques.add(entry)
 | 
			
		||||
#             return True
 | 
			
		||||
 | 
			
		||||
#     def clear(self) -> None:
 | 
			
		||||
#         """
 | 
			
		||||
#         @brief Clear this unique set for further using.
 | 
			
		||||
#         """
 | 
			
		||||
#         self.__mUniques.clear()
 | 
			
		||||
 | 
			
		||||
#     def _get_error_msg(self, err_entry: str) -> str:
 | 
			
		||||
#         """
 | 
			
		||||
#         @brief Get the error message when error occurs.
 | 
			
		||||
#         @details
 | 
			
		||||
#         This is internal used function to get the error message which will be passed to reporter.
 | 
			
		||||
#         This message is generated by given entry which cause the non-unique issue.
 | 
			
		||||
#         Outer caller should not call this function and every child class should override this function.
 | 
			
		||||
#         @param[in] err_entry The entry cause the error.
 | 
			
		||||
#         @return The error message generated from given error entry.
 | 
			
		||||
#         """
 | 
			
		||||
#         raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
# class UniqueIdentifier(UniqueField):
 | 
			
		||||
#     """Specific UniqueField for unique prototype identifier."""
 | 
			
		||||
#     def _get_error_msg(self, err_entry: str) -> str:
 | 
			
		||||
#         return f'Trying to register multiple prototype with same name: "{err_entry}".'
 | 
			
		||||
# class UniqueVariable(UniqueField):
 | 
			
		||||
#     """Specific UniqueField for unique variable names within prototype."""
 | 
			
		||||
#     def _get_error_msg(self, err_entry: str) -> str:
 | 
			
		||||
#         return f'Trying to define multiple variable with same name: "{err_entry}" in the same prototype.'
 | 
			
		||||
 | 
			
		||||
# class BMEValidator():
 | 
			
		||||
#     """
 | 
			
		||||
#     The validator for BME prototype declarartions.
 | 
			
		||||
#     This validator will validate given prototype declaration JSON structure,
 | 
			
		||||
#     to check then whether have all essential fields BME standard required and whether have any unknown fields.
 | 
			
		||||
#     """
 | 
			
		||||
 | 
			
		||||
#     __mHierarchy: bme_utils.Hierarchy
 | 
			
		||||
#     __mReporter: ReporterWithHierarchy
 | 
			
		||||
 | 
			
		||||
#     __mUniqueIdentifier: UniqueIdentifier
 | 
			
		||||
#     __mUniqueVariable: UniqueVariable
 | 
			
		||||
 | 
			
		||||
#     def __init__(self, reporter: bme_utils.Reporter):
 | 
			
		||||
#         self.__mHierarchy = bme_utils.Hierarchy()
 | 
			
		||||
#         self.__mReporter = ReporterWithHierarchy(reporter, self.__mHierarchy)
 | 
			
		||||
 | 
			
		||||
#         self.__mUniqueIdentifier = UniqueIdentifier(self.__mReporter)
 | 
			
		||||
#         self.__mUniqueVariable = UniqueVariable(self.__mReporter)
 | 
			
		||||
 | 
			
		||||
#     _TCheckKey = typing.TypeVar('_TCheckKey')
 | 
			
		||||
#     def __check_key(self, data: dict[str, typing.Any], key: str, expected_type: type[_TCheckKey]) -> _TCheckKey | None:
 | 
			
		||||
#         """
 | 
			
		||||
#         @brief Check the existance and tyoe of value stored in given dict and key.
 | 
			
		||||
#         @param[in] data The dict need to be checked
 | 
			
		||||
#         @param[in] key The key for fetching value.
 | 
			
		||||
#         @param[in] expected_type The expected type of fetched value.
 | 
			
		||||
#         @return None if error occurs, otherwise the value stored in given dict and key.
 | 
			
		||||
#         """
 | 
			
		||||
#         gotten_value = data[key]
 | 
			
		||||
#         if gotten_value is None:
 | 
			
		||||
#             # report no key error
 | 
			
		||||
#             self.__mReporter.error(f'Can not find key "{key}". Did you forget it?')
 | 
			
		||||
#         elif not isinstance(gotten_value, expected_type):
 | 
			
		||||
#             # get the type of value
 | 
			
		||||
#             value_type = type(gotten_value)
 | 
			
		||||
#             # format normal error message
 | 
			
		||||
#             err_msg: str = f'The type of value stored inside key "{key}" is incorrect. '
 | 
			
		||||
#             err_msg += f'Expect "{expected_type.__name__}" got "{value_type.__name__}". '
 | 
			
		||||
#             # add special note for easily confusing types
 | 
			
		||||
#             # e.g. forget quote number (number literal are recognise as number accidently)
 | 
			
		||||
#             if issubclass(expected_type, str) and issubclass(type(data), (int, float)):
 | 
			
		||||
#                 err_msg +=  'Did you forgot quote the number?'
 | 
			
		||||
#             # report type error
 | 
			
		||||
#             self.__mReporter.error(err_msg)
 | 
			
		||||
#         else:
 | 
			
		||||
#             # no error, return value
 | 
			
		||||
#             return gotten_value
 | 
			
		||||
#         # error occurs, return null
 | 
			
		||||
#         return None
 | 
			
		||||
 | 
			
		||||
#     def __check_self(self, data: typing.Any, expected_type: type) -> bool:
 | 
			
		||||
#         """
 | 
			
		||||
#         @brief Check the type of given data.
 | 
			
		||||
#         @return True if type matched, otherwise false.
 | 
			
		||||
#         """
 | 
			
		||||
#         if data is None:
 | 
			
		||||
#             self.__mReporter.error('Data is unexpected null.')
 | 
			
		||||
#         elif not isinstance(data, expected_type):
 | 
			
		||||
#             # usually this function is checking list or dict, so no scenario that user forget quote literal number.
 | 
			
		||||
#             self.__mReporter.error(f'The type of given data is not expected. Expect "{expected_type.__name__}" got "{type(data).__name__}".')
 | 
			
		||||
#         else:
 | 
			
		||||
#             # no error, return okey
 | 
			
		||||
#             return True
 | 
			
		||||
#         # error occurs, return failed
 | 
			
		||||
#         return False
 | 
			
		||||
 | 
			
		||||
#     # 按层次递归调用检查。
 | 
			
		||||
#     # 每个层次只负责当前层次的检查。
 | 
			
		||||
#     # 如果值为列表,字典,则在当前层次检查完其类型(容器本身,对每一项不检查),然后对每一项调用对应层次检查。
 | 
			
		||||
#     # 如果值不是上述类型(例如整数,浮点数,字符串等),在当前层次检查。
 | 
			
		||||
 | 
			
		||||
#     def validate(self, assoc_file: str, prototypes: typing.Any) -> None:
 | 
			
		||||
#         # reset hierarchy
 | 
			
		||||
#         self.__mHierarchy.clear()
 | 
			
		||||
#         # start to validate
 | 
			
		||||
#         with self.__mHierarchy.safe_push(assoc_file):
 | 
			
		||||
#             self.__validate_prototypes(prototypes)
 | 
			
		||||
 | 
			
		||||
#     def __validate_prototypes(self, prototypes: typing.Any) -> None:
 | 
			
		||||
#         # the most outer structure must be a list
 | 
			
		||||
#         if not self.__check_self(prototypes, list): return
 | 
			
		||||
#         cast_prototypes = typing.cast(list[typing.Any], prototypes)
 | 
			
		||||
#         # iterate prototype
 | 
			
		||||
#         for prototype_index, prototype in enumerate(cast_prototypes):
 | 
			
		||||
#             with self.__mHierarchy.safe_push(prototype_index) as layer:
 | 
			
		||||
#                 self.__validate_prototype(layer, prototype)
 | 
			
		||||
 | 
			
		||||
#     def __validate_prototype(self, layer: bme_utils.HierarchyLayer, prototype: typing.Any) -> None:
 | 
			
		||||
#         # check whether self is a dict
 | 
			
		||||
#         if not self.__check_self(prototype, dict): return
 | 
			
		||||
#         cast_prototype = typing.cast(dict[str, typing.Any], prototype)
 | 
			
		||||
 | 
			
		||||
#         # clear unique field for each prototype
 | 
			
		||||
#         self.__mUniqueVariable.clear()
 | 
			
		||||
 | 
			
		||||
#         # check identifier
 | 
			
		||||
#         identifier = self.__check_key(cast_prototype, TOKEN_IDENTIFIER, str)
 | 
			
		||||
#         if identifier is not None:
 | 
			
		||||
#             # replace hierarchy
 | 
			
		||||
#             layer.emplace(identifier)
 | 
			
		||||
#             # check unique
 | 
			
		||||
#             self.__mUniqueIdentifier.register(identifier)
 | 
			
		||||
 | 
			
		||||
#         # check showcase but don't use check function
 | 
			
		||||
#         # because it is optional.
 | 
			
		||||
#         showcase = cast_prototype[TOKEN_SHOWCASE]
 | 
			
		||||
#         if showcase is not None:
 | 
			
		||||
#             # we only check non-template prototype
 | 
			
		||||
#             with self.__mHierarchy.safe_push(TOKEN_SHOWCASE):
 | 
			
		||||
#                 self.__validate_showcase(typing.cast(dict[str, typing.Any], showcase))
 | 
			
		||||
 | 
			
		||||
#         # check params, vars, vertices, faces, instances
 | 
			
		||||
#         # they are all list
 | 
			
		||||
#         params = self.__check_key(cast_prototype, TOKEN_PARAMS, list)
 | 
			
		||||
#         if params is not None:
 | 
			
		||||
#             cast_params = typing.cast(list[typing.Any], params)
 | 
			
		||||
#             with self.__mHierarchy.safe_push(TOKEN_PARAMS):
 | 
			
		||||
#                 for param_index, param in enumerate(cast_params):
 | 
			
		||||
#                     with self.__mHierarchy.safe_push(param_index):
 | 
			
		||||
#                         self.__validate_param(param)
 | 
			
		||||
        
 | 
			
		||||
#         vars = self.__check_key(cast_prototype, TOKEN_VARS, list)
 | 
			
		||||
#         if vars is not None:
 | 
			
		||||
#             cast_vars = typing.cast(list[typing.Any], vars)
 | 
			
		||||
#             with self.__mHierarchy.safe_push(TOKEN_VARS):
 | 
			
		||||
#                 for var_index, var in enumerate(cast_vars):
 | 
			
		||||
#                     with self.__mHierarchy.safe_push(var_index):
 | 
			
		||||
#                         self.__validate_var(var)
 | 
			
		||||
 | 
			
		||||
#         vertices = self.__check_key(cast_prototype, TOKEN_VERTICES, list)
 | 
			
		||||
#         if vertices is not None:
 | 
			
		||||
#             cast_vertices = typing.cast(list[typing.Any], vertices)
 | 
			
		||||
#             with self.__mHierarchy.safe_push(TOKEN_VERTICES):
 | 
			
		||||
#                 for vertex_index, vertex in enumerate(cast_vertices):
 | 
			
		||||
#                     with self.__mHierarchy.safe_push(vertex_index):
 | 
			
		||||
#                         self.__validate_vertex(vertex)
 | 
			
		||||
 | 
			
		||||
#         faces = self.__check_key(cast_prototype, TOKEN_FACES, list)
 | 
			
		||||
#         if faces is not None:
 | 
			
		||||
#             cast_faces = typing.cast(list[typing.Any], faces)
 | 
			
		||||
#             with self.__mHierarchy.safe_push(TOKEN_FACES):
 | 
			
		||||
#                 for face_index, face in enumerate(cast_faces):
 | 
			
		||||
#                     with self.__mHierarchy.safe_push(face_index):
 | 
			
		||||
#                         self.__validate_face(face)
 | 
			
		||||
 | 
			
		||||
#         instances = self.__check_key(cast_prototype, TOKEN_INSTANCES, list)
 | 
			
		||||
#         if instances is not None:
 | 
			
		||||
#             cast_instances = typing.cast(list[typing.Any], instances)
 | 
			
		||||
#             with self.__mHierarchy.safe_push(TOKEN_INSTANCES):
 | 
			
		||||
#                 for instance_index, instance in enumerate(cast_instances):
 | 
			
		||||
#                     with self.__mHierarchy.safe_push(instance_index):
 | 
			
		||||
#                         self.__validate_instance(instance)
 | 
			
		||||
 | 
			
		||||
#     def __validate_showcase(self, showcase: dict[str, typing.Any]) -> None:
 | 
			
		||||
#         pass
 | 
			
		||||
 | 
			
		||||
#     def __validate_param(self, param: typing.Any) -> None:
 | 
			
		||||
#         # check whether self is a dict
 | 
			
		||||
#         if not self.__check_self(param, dict): return
 | 
			
		||||
#         cast_param = typing.cast(dict[str, typing.Any], param)
 | 
			
		||||
 | 
			
		||||
#         # check field
 | 
			
		||||
#         field = self.__check_key(cast_param, TOKEN_PARAMS_FIELD, str)
 | 
			
		||||
#         if field is not None:
 | 
			
		||||
#             self.__mUniqueVariable.register(field)
 | 
			
		||||
 | 
			
		||||
#         # check data
 | 
			
		||||
#         self.__check_key(cast_param, TOKEN_PARAMS_DATA, str)
 | 
			
		||||
 | 
			
		||||
#     def __validate_var(self, var: typing.Any) -> None:
 | 
			
		||||
#         # check whether self is a dict
 | 
			
		||||
#         if not self.__check_self(var, dict): return
 | 
			
		||||
#         cast_var = typing.cast(dict[str, typing.Any], var)
 | 
			
		||||
 | 
			
		||||
#         # check field
 | 
			
		||||
#         field = self.__check_key(cast_var, TOKEN_VARS_FIELD, str)
 | 
			
		||||
#         if field is not None:
 | 
			
		||||
#             self.__mUniqueVariable.register(field)
 | 
			
		||||
 | 
			
		||||
#         # check data
 | 
			
		||||
#         self.__check_key(cast_var, TOKEN_VARS_DATA, str)
 | 
			
		||||
 | 
			
		||||
#     def __validate_vertex(self, vertex: typing.Any) -> None:
 | 
			
		||||
#         # check whether self is a dict
 | 
			
		||||
#         if not self.__check_self(vertex, dict): return
 | 
			
		||||
#         cast_vertex = typing.cast(dict[str, typing.Any], vertex)
 | 
			
		||||
 | 
			
		||||
#         # check fields
 | 
			
		||||
#         self.__check_key(cast_vertex, TOKEN_VERTICES_SKIP, str)
 | 
			
		||||
#         self.__check_key(cast_vertex, TOKEN_VERTICES_DATA, str)
 | 
			
		||||
 | 
			
		||||
#     def __validate_face(self, face: typing.Any) -> None:
 | 
			
		||||
#         pass
 | 
			
		||||
 | 
			
		||||
#     def __validate_instance(self, instance: typing.Any) -> None:
 | 
			
		||||
#         pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BMEExtractor():
 | 
			
		||||
    """
 | 
			
		||||
    A GetText extractor for BME prototype declarations.
 | 
			
		||||
    This extractor can extract all UI infomations which will be shown on Blender first.
 | 
			
		||||
    Then write them into caller given PO file. So that translator can translate them.
 | 
			
		||||
 | 
			
		||||
    Blender default I18N plugin can not recognise these dynamic loaded content,
 | 
			
		||||
    so that's the reason why this class invented.
 | 
			
		||||
 | 
			
		||||
    Please note all data should be validate first, then pass to this class.
 | 
			
		||||
    Otherwise it is undefined behavior.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    __mAssocFile: str
 | 
			
		||||
    __mHierarchy: bme_utils.Hierarchy
 | 
			
		||||
    __mReporter: bme_utils.Reporter
 | 
			
		||||
    __mPoWriter: simple_po.PoWriter
 | 
			
		||||
 | 
			
		||||
    def __init__(self, reporter: bme_utils.Reporter, po_writer: simple_po.PoWriter):
 | 
			
		||||
        self.__mAssocFile = ''
 | 
			
		||||
        self.__mHierarchy = bme_utils.Hierarchy()
 | 
			
		||||
        self.__mReporter = reporter
 | 
			
		||||
        self.__mPoWriter = po_writer
 | 
			
		||||
 | 
			
		||||
    def __add_translation(self, msg: str) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Convenient internal translation adder.
 | 
			
		||||
        @details Add given message into PO file with auto generated hierarchy for translation context.
 | 
			
		||||
        @param[in] msg The message for translating.
 | 
			
		||||
        """
 | 
			
		||||
        self.__mPoWriter.add_entry(
 | 
			
		||||
            msg,
 | 
			
		||||
            CTX_TRANSLATION + '/' + self.__mHierarchy.build_hierarchy_string(),
 | 
			
		||||
            # use associated file as extracted message to tell user where we extract it.
 | 
			
		||||
            # put file name in hierarchy is not proper (file path may be changed when moving prototype between them).
 | 
			
		||||
            self.__mAssocFile
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __report_duplication_error(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Convenient internal function to report duplicated translation message issue.
 | 
			
		||||
        @details
 | 
			
		||||
        A convenient internal used function to report issue that
 | 
			
		||||
        the "title" field and "desc" field of the same showcase configuration entry have same content
 | 
			
		||||
        which may cause that generated PO file is illegal.
 | 
			
		||||
        """
 | 
			
		||||
        self.__mReporter.error(
 | 
			
		||||
            'The content of "title" and "desc" can not be the same in one entry. Please modify one of them.',
 | 
			
		||||
            self.__mAssocFile + '/' + self.__mHierarchy.build_hierarchy_string()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def extract(self, assoc_file: str, prototypes: list[dict[str, typing.Any]]) -> None:
 | 
			
		||||
        self.__mAssocFile = assoc_file
 | 
			
		||||
        for prototype in prototypes:
 | 
			
		||||
            self.__extract_prototype(prototype)
 | 
			
		||||
 | 
			
		||||
    def __extract_prototype(self, prototype: dict[str, typing.Any]) -> None:
 | 
			
		||||
        # get identifier first
 | 
			
		||||
        identifier: str = prototype[TOKEN_IDENTIFIER]
 | 
			
		||||
        with self.__mHierarchy.safe_push(identifier):
 | 
			
		||||
            # get showcase node and only write PO file if it is not template prototype
 | 
			
		||||
            showcase: dict[str, typing.Any] | None = prototype[TOKEN_SHOWCASE]
 | 
			
		||||
            if showcase is not None:
 | 
			
		||||
                self.__extract_showcase(showcase)
 | 
			
		||||
 | 
			
		||||
    def __extract_showcase(self, showcase: dict[str, typing.Any]) -> None:
 | 
			
		||||
        # export self name first
 | 
			
		||||
        self.__add_translation(showcase[TOKEN_SHOWCASE_TITLE])
 | 
			
		||||
 | 
			
		||||
        # iterate cfgs
 | 
			
		||||
        cfgs: list[dict[str, typing.Any]] = showcase[TOKEN_SHOWCASE_CFGS]
 | 
			
		||||
        for cfg_index, cfg in enumerate(cfgs):
 | 
			
		||||
            self.__extract_showcase_cfg(cfg_index, cfg)
 | 
			
		||||
 | 
			
		||||
    def __extract_showcase_cfg(self, index: int, cfg: dict[str, typing.Any]) -> None:
 | 
			
		||||
        # push cfg index
 | 
			
		||||
        with self.__mHierarchy.safe_push(index):
 | 
			
		||||
            # extract field title and description
 | 
			
		||||
            title: str = cfg[TOKEN_SHOWCASE_CFGS_TITLE]
 | 
			
		||||
            desc: str = cfg[TOKEN_SHOWCASE_CFGS_DESC]
 | 
			
		||||
 | 
			
		||||
            # check duplication error
 | 
			
		||||
            # if "title" is equal to "desc" and they are not blank
 | 
			
		||||
            if title == desc and title != "":
 | 
			
		||||
                self.__report_duplication_error()
 | 
			
		||||
 | 
			
		||||
            # export them respectively if they are not blank
 | 
			
		||||
            if title != "":
 | 
			
		||||
                self.__add_translation(title)
 | 
			
		||||
            if desc!= "":
 | 
			
		||||
                self.__add_translation(desc)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										144
									
								
								scripts/bme_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								scripts/bme_utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,144 @@
 | 
			
		||||
import typing
 | 
			
		||||
import collections
 | 
			
		||||
import termcolor
 | 
			
		||||
 | 
			
		||||
class Reporter():
 | 
			
		||||
    """
 | 
			
		||||
    General reporter with context support for convenient logging.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def __report(self, type: str, msg: str, context: str | None, color: str) -> None:
 | 
			
		||||
        # build message
 | 
			
		||||
        strl: str = f'[{type}]'
 | 
			
		||||
        if context is not None:
 | 
			
		||||
            strl += f'[{context}]'
 | 
			
		||||
        strl += ' ' + msg
 | 
			
		||||
        # output with color
 | 
			
		||||
        termcolor.cprint(strl, color)
 | 
			
		||||
 | 
			
		||||
    def error(self, msg: str, context: str | None = None) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Report an error.
 | 
			
		||||
        @param[in] msg The message to show.
 | 
			
		||||
        @param[in] context The context of this message, e.g. the file path. None if no context.
 | 
			
		||||
        """
 | 
			
		||||
        self.__report('Error', msg, context, 'red')
 | 
			
		||||
 | 
			
		||||
    def warning(self, msg: str, context: str | None = None) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Report a warning.
 | 
			
		||||
        @param[in] msg The message to show.
 | 
			
		||||
        @param[in] context The context of this message, e.g. the file path. None if no context.
 | 
			
		||||
        """
 | 
			
		||||
        self.__report('Warning', msg, context, 'yellow')
 | 
			
		||||
 | 
			
		||||
    def info(self, msg: str, context: str | None = None) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Report a info.
 | 
			
		||||
        @param[in] msg The message to show.
 | 
			
		||||
        @param[in] context The context of this message, e.g. the file path. None if no context.
 | 
			
		||||
        """
 | 
			
		||||
        self.__report('Info', msg, context, 'white')
 | 
			
		||||
 | 
			
		||||
class Hierarchy():
 | 
			
		||||
    """
 | 
			
		||||
    The hierarchy for BME validator and BME extractor.
 | 
			
		||||
    In BME validator, it build human-readable string representing the location where error happen.
 | 
			
		||||
    In BME extractor, it build the string used as the context of translation.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    __mStack: collections.deque[str]
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.__mStack = collections.deque()
 | 
			
		||||
 | 
			
		||||
    def push(self, item: str | int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Add an item into the top of this hierarchy.
 | 
			
		||||
        @details
 | 
			
		||||
        If given item is string, it will be push into hierarchy directly.
 | 
			
		||||
        If given item is integer, this function will treat it as a special case, the index.
 | 
			
		||||
        Function will push it into hierarchy after formatting it (add a pair of bracket around it).
 | 
			
		||||
        @param[in] item New added item.
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(item, str):
 | 
			
		||||
            self.__mStack.append(item)
 | 
			
		||||
        elif isinstance(item, int):
 | 
			
		||||
            self.__mStack.append(f'[{item}]')
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception('Unexpected type of item when pushing into hierarchy.')
 | 
			
		||||
 | 
			
		||||
    def pop(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Remove the top item from hierarchy
 | 
			
		||||
        """
 | 
			
		||||
        self.__mStack.pop()
 | 
			
		||||
 | 
			
		||||
    def safe_push(self, item: str | int) -> 'HierarchyLayer':
 | 
			
		||||
        """
 | 
			
		||||
        @brief The safe version of push function.
 | 
			
		||||
        @return A with-context-supported instance which can make sure pushed item popped when leaving scope.
 | 
			
		||||
        """
 | 
			
		||||
        return HierarchyLayer(self, item)
 | 
			
		||||
 | 
			
		||||
    def clear(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Clear this hierarchy.
 | 
			
		||||
        """
 | 
			
		||||
        self.__mStack.clear()
 | 
			
		||||
 | 
			
		||||
    def depth(self) -> int:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Return the depth of this hierarchy.
 | 
			
		||||
        @return The depth of this hierarchy.
 | 
			
		||||
        """
 | 
			
		||||
        return len(self.__mStack)
 | 
			
		||||
 | 
			
		||||
    def build_hierarchy_string(self) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Build the string which can represent this hierarchy.
 | 
			
		||||
        @details It just join every items with `/` as separator.
 | 
			
		||||
        @return The built string representing this hierarchy.
 | 
			
		||||
        """
 | 
			
		||||
        return '/'.join(self.__mStack)
 | 
			
		||||
 | 
			
		||||
class HierarchyLayer():
 | 
			
		||||
    """
 | 
			
		||||
    An with-context-supported class for Hierarchy which can automatically pop item when leaving scope.
 | 
			
		||||
    This is convenient for keeping the balance of Hierarchy (avoid programmer accidently forgetting to pop item).
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    __mHasPop: bool
 | 
			
		||||
    __mAssocHierarchy: Hierarchy
 | 
			
		||||
 | 
			
		||||
    def __init__(self, assoc_hierarchy: Hierarchy, item: str | int):
 | 
			
		||||
        self.__mAssocHierarchy = assoc_hierarchy
 | 
			
		||||
        self.__mHasPop = False
 | 
			
		||||
        self.__mAssocHierarchy.push(item)
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        return self
 | 
			
		||||
    
 | 
			
		||||
    def __exit__(self, exc_type, exc_value, traceback):
 | 
			
		||||
        self.close()
 | 
			
		||||
    
 | 
			
		||||
    def close(self) -> None:
 | 
			
		||||
        if not self.__mHasPop:
 | 
			
		||||
            self.__mAssocHierarchy.pop()
 | 
			
		||||
            self.__mHasPop = True
 | 
			
		||||
 | 
			
		||||
    def emplace(self, new_item: str | int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Replace the content of top item in-place.
 | 
			
		||||
        @details
 | 
			
		||||
        In some cases, caller need to replace the content of top item.
 | 
			
		||||
        For example, at the beginning, we only have index info.
 | 
			
		||||
        After validating something, we can fetching a more human-readable info, such as name,
 | 
			
		||||
        now we need replace the content of top item.
 | 
			
		||||
        @param[in] new_item The new content of top item.
 | 
			
		||||
        """
 | 
			
		||||
        self.__mAssocHierarchy.pop()
 | 
			
		||||
        self.__mAssocHierarchy.push(new_item)
 | 
			
		||||
							
								
								
									
										51
									
								
								scripts/build_icons.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								scripts/build_icons.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
import logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import common
 | 
			
		||||
import PIL, PIL.Image
 | 
			
		||||
 | 
			
		||||
# the config for thumbnail
 | 
			
		||||
THUMBNAIL_SIZE: int = 16
 | 
			
		||||
 | 
			
		||||
class ThumbnailBuilder():
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def build_thumbnails(self) -> None:
 | 
			
		||||
        # get folder path
 | 
			
		||||
        root_folder = common.get_plugin_folder()
 | 
			
		||||
 | 
			
		||||
        # prepare handler
 | 
			
		||||
        def folder_handler(rel_name: str, src_folder: Path, dst_folder: Path) -> None:
 | 
			
		||||
            # just create folder
 | 
			
		||||
            logging.info(f'Creating Folder: {src_folder} -> {dst_folder}')
 | 
			
		||||
            dst_folder.mkdir(parents=False, exist_ok=True)
 | 
			
		||||
        def file_handler(rel_name: str, src_file: Path, dst_file: Path) -> None:
 | 
			
		||||
            # skip non-image
 | 
			
		||||
            if src_file.suffix != '.png': return
 | 
			
		||||
            # call thumbnail func
 | 
			
		||||
            logging.info(f'Building Thumbnail: {src_file} -> {dst_file}')
 | 
			
		||||
            self.__resize_image(src_file, dst_file)
 | 
			
		||||
 | 
			
		||||
        # call common processor
 | 
			
		||||
        common.common_file_migrator(
 | 
			
		||||
            root_folder / 'raw_icons',
 | 
			
		||||
            root_folder / 'icons',
 | 
			
		||||
            folder_handler,
 | 
			
		||||
            file_handler
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        logging.info('Building thumbnail done.')
 | 
			
		||||
 | 
			
		||||
    def __resize_image(self, src_file: Path, dst_file: Path) -> None:
 | 
			
		||||
        # open image
 | 
			
		||||
        src_image: PIL.Image.Image = PIL.Image.open(src_file)
 | 
			
		||||
        # create thumbnail
 | 
			
		||||
        src_image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE))
 | 
			
		||||
        # save to new file
 | 
			
		||||
        src_image.save(dst_file)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    common.setup_logging()
 | 
			
		||||
    thumbnail_builder = ThumbnailBuilder()
 | 
			
		||||
    thumbnail_builder.build_thumbnails()
 | 
			
		||||
							
								
								
									
										84
									
								
								scripts/build_jsons.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								scripts/build_jsons.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
import os, json, typing
 | 
			
		||||
import bme_utils, bme_relatives, simple_po
 | 
			
		||||
import common
 | 
			
		||||
 | 
			
		||||
class JsonCompressor():
 | 
			
		||||
 | 
			
		||||
    __mReporter: bme_utils.Reporter
 | 
			
		||||
    __mPoWriter: simple_po.PoWriter
 | 
			
		||||
    # __mValidator: bme_relatives.BMEValidator
 | 
			
		||||
    __mExtractor: bme_relatives.BMEExtractor
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.__mReporter = bme_utils.Reporter()
 | 
			
		||||
        self.__mPoWriter = simple_po.PoWriter(
 | 
			
		||||
            os.path.join(common.get_plugin_folder(), 'i18n', 'bme.pot'), 
 | 
			
		||||
            'BME Prototypes'
 | 
			
		||||
        )
 | 
			
		||||
        # self.__mValidator = bme_relatives.BMEValidator(self.__mReporter)
 | 
			
		||||
        self.__mExtractor = bme_relatives.BMEExtractor(self.__mReporter, self.__mPoWriter)
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        return self
 | 
			
		||||
    
 | 
			
		||||
    def __exit__(self, exc_type, exc_value, traceback):
 | 
			
		||||
        self.close()
 | 
			
		||||
    
 | 
			
		||||
    def close(self) -> None:
 | 
			
		||||
        self.__mPoWriter.close()
 | 
			
		||||
 | 
			
		||||
    def run(self) -> None:
 | 
			
		||||
        self.__compress_jsons()
 | 
			
		||||
 | 
			
		||||
    def __compress_jsons(self) -> None:
 | 
			
		||||
        # get folder path
 | 
			
		||||
        root_folder: str = common.get_plugin_folder()
 | 
			
		||||
 | 
			
		||||
        # prepare handler
 | 
			
		||||
        def folder_handler(rel_name: str, src_folder: str, dst_folder: str) -> None:
 | 
			
		||||
            # just create folder
 | 
			
		||||
            self.__mReporter.info(f'Creating Folder: {src_folder} -> {dst_folder}')
 | 
			
		||||
            os.makedirs(dst_folder, exist_ok = True)
 | 
			
		||||
        def file_handler(rel_name: str, src_file: str, dst_file: str) -> None:
 | 
			
		||||
            # skip non-json
 | 
			
		||||
            if not src_file.endswith('.json'): return
 | 
			
		||||
            # call compress func
 | 
			
		||||
            self.__mReporter.info(f'Processing JSON: {src_file} -> {dst_file}')
 | 
			
		||||
            self.__compress_json(rel_name, src_file, dst_file)
 | 
			
		||||
 | 
			
		||||
        # call common processor
 | 
			
		||||
        common.common_file_migrator(
 | 
			
		||||
            os.path.join(root_folder, 'raw_jsons'),
 | 
			
		||||
            os.path.join(root_folder, 'jsons'),
 | 
			
		||||
            folder_handler,
 | 
			
		||||
            file_handler
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.__mReporter.info('Building JSON done.')
 | 
			
		||||
 | 
			
		||||
    def __compress_json(self, rel_name: str, src_file: str, dst_file: str) -> None:
 | 
			
		||||
        # load data first
 | 
			
		||||
        loaded_prototypes: typing.Any
 | 
			
		||||
        with open(src_file, 'r', encoding = 'utf-8') as fr:
 | 
			
		||||
            loaded_prototypes = json.load(fr)
 | 
			
		||||
 | 
			
		||||
        # validate loaded data
 | 
			
		||||
        # self.__mValidator.validate(rel_name, loaded_prototypes)
 | 
			
		||||
 | 
			
		||||
        # extract translation
 | 
			
		||||
        self.__mExtractor.extract(rel_name, loaded_prototypes)
 | 
			
		||||
 | 
			
		||||
        # save result
 | 
			
		||||
        with open(dst_file, 'w', encoding = 'utf-8') as fw:
 | 
			
		||||
            json.dump(
 | 
			
		||||
                loaded_prototypes,  # loaded data
 | 
			
		||||
                fw,
 | 
			
		||||
                indent = None,  # no indent. the most narrow style.
 | 
			
		||||
                separators = (',', ':'),    # also for narrow style.
 | 
			
		||||
                sort_keys = False,  # do not sort key
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    with JsonCompressor() as json_compressor:
 | 
			
		||||
        json_compressor.run()
 | 
			
		||||
							
								
								
									
										79
									
								
								scripts/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								scripts/common.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
			
		||||
import os, typing, logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
def get_plugin_folder() -> Path:
 | 
			
		||||
    """
 | 
			
		||||
    Get the absolute path to plugin root folder.
 | 
			
		||||
 | 
			
		||||
    :return: The absolute path to plugin root folder.
 | 
			
		||||
    """
 | 
			
		||||
    return Path(__file__).resolve().parent.parent
 | 
			
		||||
 | 
			
		||||
def relative_to_folder(abs_path: Path, src_parent: Path, dst_parent: Path) -> Path:
 | 
			
		||||
    """
 | 
			
		||||
    Rebase one path to another path.
 | 
			
		||||
 | 
			
		||||
    Give a absolute file path and folder path, and compute the relative path of given file to given folder.
 | 
			
		||||
    Then applied the computed relative path to another given folder path.
 | 
			
		||||
    Thus it seems like the file was rebased to from a folder to another folder with keeping the folder hierarchy.
 | 
			
		||||
 | 
			
		||||
    For example, given `/path/to/file` and `/path`, it will compute relative path `to/file`.
 | 
			
		||||
    Then it was applied to another folder path `/new` and got `/new/to/file`.
 | 
			
		||||
 | 
			
		||||
    :param abs_path: The absolute path to a folder or file.
 | 
			
		||||
    :param src_parent: The absolute path to folder which the `abs_path` will have relative path to.
 | 
			
		||||
    :param dst_parent: The absolute path to folder which the relative path will be applied to.
 | 
			
		||||
    """
 | 
			
		||||
    return dst_parent / (abs_path.relative_to(src_parent))
 | 
			
		||||
 | 
			
		||||
def common_file_migrator(
 | 
			
		||||
        from_folder: Path, to_folder: Path,
 | 
			
		||||
        fct_proc_folder: typing.Callable[[str, Path, Path], None],
 | 
			
		||||
        fct_proc_file: typing.Callable[[str, Path, Path], None]) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Common file migrator used by some build script.
 | 
			
		||||
 | 
			
		||||
    This function receive 2 absolute folder path. `from_folder` indicate the file migrated out,
 | 
			
		||||
    and `to_folder` indicate the file migrated in.
 | 
			
		||||
    `fct_proc_folder` is a function pointer from caller which handle folder migration in detail.
 | 
			
		||||
    `fct_proc_file` is same but handle file migration.
 | 
			
		||||
 | 
			
		||||
    `fct_proc_folder` will receive 3 args.
 | 
			
		||||
    First is the name of this folder which can be shown for end user.
 | 
			
		||||
    Second is the source folder and third is expected dest folder.
 | 
			
		||||
    `fct_proc_file` is same, but receive the file path instead.
 | 
			
		||||
    Both of these function pointer should do the migration in detail. This function will only just iterate
 | 
			
		||||
    folder and give essential args and will not do any migration operations such as copying or moving.
 | 
			
		||||
 | 
			
		||||
    :param from_folder: The folder need to be migrated.
 | 
			
		||||
    :param to_folder: The folder will be migrated to.
 | 
			
		||||
    :param fct_proc_folder: Folder migration detail handler.
 | 
			
		||||
    :param fct_proc_file: File migration detail handler.
 | 
			
		||||
    """
 | 
			
		||||
    # TODO: If we have Python 3.12, use Path.walk instead of current polyfill.
 | 
			
		||||
 | 
			
		||||
    # iterate from_folder folder
 | 
			
		||||
    for root, dirs, files in os.walk(from_folder, topdown=True):
 | 
			
		||||
        root = Path(root)
 | 
			
		||||
 | 
			
		||||
        # iterate folders
 | 
			
		||||
        for name in dirs:
 | 
			
		||||
            # prepare handler args
 | 
			
		||||
            src_folder = root / name
 | 
			
		||||
            dst_folder = relative_to_folder(src_folder, from_folder, to_folder)
 | 
			
		||||
            # call handler
 | 
			
		||||
            fct_proc_folder(name, src_folder, dst_folder)
 | 
			
		||||
 | 
			
		||||
        # iterate files
 | 
			
		||||
        for name in files:
 | 
			
		||||
            # prepare handler args
 | 
			
		||||
            src_file = root / name
 | 
			
		||||
            dst_file = relative_to_folder(src_file, from_folder, to_folder)
 | 
			
		||||
            # call handler
 | 
			
		||||
            fct_proc_file(name, src_file, dst_file)
 | 
			
		||||
 | 
			
		||||
def setup_logging() -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Setup uniform style for logging module.
 | 
			
		||||
    """
 | 
			
		||||
    logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
 | 
			
		||||
							
								
								
									
										6
									
								
								scripts/extract_json.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								scripts/extract_json.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
import common
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    common.setup_logging()
 | 
			
		||||
							
								
								
									
										9
									
								
								scripts/pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								scripts/pyproject.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
[project]
 | 
			
		||||
name = "scripts"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
requires-python = ">=3.11"
 | 
			
		||||
dependencies = [
 | 
			
		||||
    "pillow==10.2.0",
 | 
			
		||||
    "polib>=1.2.0",
 | 
			
		||||
    "pydantic>=2.11.7",
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										99
									
								
								scripts/simple_po.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								scripts/simple_po.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
			
		||||
import typing
 | 
			
		||||
import io
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
class PoWriter():
 | 
			
		||||
    """
 | 
			
		||||
    The simple PO file writer.
 | 
			
		||||
    This class is just served for writing POT files.
 | 
			
		||||
    It may be convenient when exporting PO file for thoese whose format can not be parsed by formal tools.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    __cEscapeCharsDict: typing.ClassVar[dict[str, str]] = {
 | 
			
		||||
        '\\': '\\\\',
 | 
			
		||||
        '"': '\\"',
 | 
			
		||||
        '\n': '\\n',
 | 
			
		||||
        '\t': '\\t',
 | 
			
		||||
    }
 | 
			
		||||
    __cEscapeCharsTable: typing.ClassVar[dict] = str.maketrans(__cEscapeCharsDict)
 | 
			
		||||
    __mPoFile: io.TextIOWrapper
 | 
			
		||||
 | 
			
		||||
    def __init__(self, po_file_path: str, project_name: str):
 | 
			
		||||
        # open file
 | 
			
		||||
        self.__mPoFile = open(po_file_path, 'w', encoding = 'utf-8')
 | 
			
		||||
        # add default header
 | 
			
		||||
        self.__add_header(project_name)
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        return self
 | 
			
		||||
    
 | 
			
		||||
    def __exit__(self, exc_type, exc_value, traceback):
 | 
			
		||||
        self.close()
 | 
			
		||||
    
 | 
			
		||||
    def close(self) -> None:
 | 
			
		||||
        self.__mPoFile.close()
 | 
			
		||||
 | 
			
		||||
    def __write_line(self, val: str) -> None:
 | 
			
		||||
        self.__mPoFile.write(val)
 | 
			
		||||
        self.__mPoFile.write('\n')
 | 
			
		||||
 | 
			
		||||
    def __escape_str(self, val: str) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        This function escapes a given string to make it safe to use as a C++ string literal.
 | 
			
		||||
        @param[in] val Original string
 | 
			
		||||
        @return Escaped string
 | 
			
		||||
        """
 | 
			
		||||
        return val.translate(PoWriter.__cEscapeCharsTable)
 | 
			
		||||
 | 
			
		||||
    def __add_header(self, project_name: str) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Add default header for PO file.
 | 
			
		||||
        @param[in] project_name The project name written in file.
 | 
			
		||||
        """
 | 
			
		||||
        now_datetime = datetime.datetime.now()
 | 
			
		||||
        self.__write_line('# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.')
 | 
			
		||||
        self.__write_line('msgid ""')
 | 
			
		||||
        self.__write_line('msgstr ""')
 | 
			
		||||
        self.__write_line(f'"Project-Id-Version: {self.__escape_str(project_name)}\\n"')
 | 
			
		||||
        self.__write_line(f'"POT-Creation-Date: {now_datetime.strftime("%Y-%m-%d %H:%M%Z")}\\n"')
 | 
			
		||||
        self.__write_line('"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"')
 | 
			
		||||
        self.__write_line('"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"')
 | 
			
		||||
        self.__write_line('"Language-Team: LANGUAGE <LL@li.org>\\n"')
 | 
			
		||||
        self.__write_line('"Language: __POT__\\n"')
 | 
			
		||||
        self.__write_line('"MIME-Version: 1.0\\n"')
 | 
			
		||||
        self.__write_line('"Content-Type: text/plain; charset=UTF-8\\n"')
 | 
			
		||||
        self.__write_line('"Content-Transfer-Encoding: 8bit\\n"')
 | 
			
		||||
        self.__write_line('"X-Generator: simple_po.PoWriter\\n"')
 | 
			
		||||
 | 
			
		||||
    def add_entry(self, msg: str, msg_context: str | None = None, extracted_comment: str | None = None, reference: str | None = None) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        @brief Write an entry into PO file with given arguments.
 | 
			
		||||
        @details
 | 
			
		||||
        Please note this function will NOT check whether there already is a duplicated entry which has been written.
 | 
			
		||||
        You must check this on your own.
 | 
			
		||||
        @param[in] msg The message string need to be translated.
 | 
			
		||||
        @param[in] msg_context The context of this message.
 | 
			
		||||
        @param[in] extracted_comment The extracted comment of this message. None if no reference. Line breaker is not allowed.
 | 
			
		||||
        @param[in] reference The code refernece of this message. None if no reference. Line breaker is not allowed.
 | 
			
		||||
        """
 | 
			
		||||
        # empty string will not be translated
 | 
			
		||||
        if msg == '': return
 | 
			
		||||
 | 
			
		||||
        # write blank line first
 | 
			
		||||
        self.__write_line('')
 | 
			
		||||
        if extracted_comment:
 | 
			
		||||
            self.__write_line(f'#. {extracted_comment}')
 | 
			
		||||
        if reference:
 | 
			
		||||
            self.__write_line(f'#: {reference}')
 | 
			
		||||
        if msg_context:
 | 
			
		||||
            self.__write_line(f'msgctxt "{self.__escape_str(msg_context)}"')
 | 
			
		||||
        self.__write_line(f'msgid "{self.__escape_str(msg)}"')
 | 
			
		||||
        self.__write_line('msgstr ""')
 | 
			
		||||
 | 
			
		||||
    def build_code_reference(self, code_file_path: str, code_line_number: int) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        A convenient function to build code reference string used when adding entry.
 | 
			
		||||
        @param[in] code_file_path The path to associated code file.
 | 
			
		||||
        @param[in] code_line_number The line number of associated code within given file.
 | 
			
		||||
        """
 | 
			
		||||
        return f'{code_file_path}:{code_line_number}'
 | 
			
		||||
							
								
								
									
										169
									
								
								scripts/uv.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								scripts/uv.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,169 @@
 | 
			
		||||
version = 1
 | 
			
		||||
revision = 2
 | 
			
		||||
requires-python = ">=3.11"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "annotated-types"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pillow"
 | 
			
		||||
version = "10.2.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/3e/32cbd0129a28686621434cbf17bb64bf1458bfb838f1f668262fefce145c/pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", size = 46212712, upload-time = "2024-01-02T09:16:59.702Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/89/1d/23bafc80495b2a902b27d242e9226ea0b74624f108c60f0533329c051f78/pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", size = 3518211, upload-time = "2024-01-02T09:15:21.874Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/46/ce/a84284ab66a278825109b03765d7411be3ff18250da44faa9fb5ea9a16a0/pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", size = 3318744, upload-time = "2024-01-02T09:15:24.732Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2c/36/57c68f5d03b471c4bd7302821b4fcb6f126ba91f78b590ffce00a8c2ac42/pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", size = 4304573, upload-time = "2024-01-02T09:32:51.962Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a5/23/3c59ba2bb48f2ab2f11c3597f50458f63ed46dcc4cedd3308f6e4ec7271f/pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", size = 4414949, upload-time = "2024-01-02T09:15:27.503Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/18/6c/04ef8c00c258df1f0f4ef940d76bc278d15693fbb3268da00b9f4b145ad6/pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", size = 4328040, upload-time = "2024-01-02T09:32:56.979Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/66/9c/2e1877630eb298bbfd23f90deeec0a3f682a4163d5ca9f178937de57346c/pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", size = 4494803, upload-time = "2024-01-02T09:15:30.346Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/09/1f/b01ddb19acb325f1ee569cae9b914ce30f589f43d089e572ec6fd632f560/pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", size = 4520153, upload-time = "2024-01-02T09:33:01.573Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ae/94/340ca3ee7b632c2019498e0f1d399530152f8c4e39f8374ace2fec147322/pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", size = 4585627, upload-time = "2024-01-02T09:15:33.069Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/73/89/bef0d3a0e0c2cc054e055a38ca1ac210749b9537cb13b10f6fe0343eed79/pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", size = 2289835, upload-time = "2024-01-02T09:15:35.027Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/43/56/f92715a873187b5eff72a4a0d2ac6258e18e9bfb0e136aafde65c49a841a/pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", size = 2621395, upload-time = "2024-01-02T09:15:37.42Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b1/71/eea5f690e5f8d77cdde455d7e42bae0a2d918bec886f0e7fefb6836c51f4/pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", size = 2229075, upload-time = "2024-01-02T09:15:39.285Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/37/d5/2c00228ace73a7855a52053a92fdd6cea9b22393fbf3961125c11829dcd2/pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", size = 3517780, upload-time = "2024-01-02T09:15:41.495Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9d/a0/28756da34d6b58c3c5f6c1d5589e4e8f4e73472b55875524ae9d6e7e98fe/pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", size = 3317920, upload-time = "2024-01-02T09:15:44.116Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ab/72/e6a8887c0ce6c94cd0b74fef495a81f4ea4c742242de4bc1943abbd21f92/pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", size = 4308358, upload-time = "2024-01-02T09:33:09.603Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a8/2f/86cf1dc4b0530e4c3e96edd0338dcc4809c2622d9d45460029a71a831473/pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", size = 4422007, upload-time = "2024-01-02T09:15:46.355Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/00/43/1ca3313b56ef623de0afebfe3d7a6e9c07e1a76c50ce191302018907b2b5/pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", size = 4333841, upload-time = "2024-01-02T09:33:14.842Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5c/c6/5b6b1f7362267494a423b45af684d604491565e81436e3ebeefee68f78fd/pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", size = 4502101, upload-time = "2024-01-02T09:15:48.416Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e6/c5/37e72d74c248adf133a2dd56890cf8632e2e46562e5fa70414445bbd3ae6/pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", size = 4542122, upload-time = "2024-01-02T09:33:19.012Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/fa/93/79979b8ab99da2958bf6fef1be745c344c4e727f07d1429c49c015e21db2/pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", size = 4611042, upload-time = "2024-01-02T09:15:50.616Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ce/a7/11a539c1e12dfb9d67c35e5d3d99c7a6853face9083e6483360f4d9cd1d8/pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", size = 2290438, upload-time = "2024-01-02T09:15:53.219Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/51/07/7e9266a59bb267b56c1f432f6416653b9a78dda771c57740d064a8aa2a44/pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", size = 2621845, upload-time = "2024-01-02T09:15:55.293Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a0/61/6cff8a8dbbac3d7fb7adb435b60737a7d0b0849f53e3af38f2c94d988da6/pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", size = 2229322, upload-time = "2024-01-02T09:15:57.475Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "polib"
 | 
			
		||||
version = "1.2.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/10/9a/79b1067d27e38ddf84fe7da6ec516f1743f31f752c6122193e7bce38bdbf/polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b", size = 161658, upload-time = "2023-02-23T17:53:56.873Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6b/99/45bb1f9926efe370c6dbe324741c749658e44cb060124f28dad201202274/polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d", size = 20634, upload-time = "2023-02-23T17:53:59.919Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pydantic"
 | 
			
		||||
version = "2.11.7"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "annotated-types" },
 | 
			
		||||
    { name = "pydantic-core" },
 | 
			
		||||
    { name = "typing-extensions" },
 | 
			
		||||
    { name = "typing-inspection" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pydantic-core"
 | 
			
		||||
version = "2.33.2"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "typing-extensions" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tools"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
source = { virtual = "." }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "pillow" },
 | 
			
		||||
    { name = "polib" },
 | 
			
		||||
    { name = "pydantic" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.metadata]
 | 
			
		||||
requires-dist = [
 | 
			
		||||
    { name = "pillow", specifier = "==10.2.0" },
 | 
			
		||||
    { name = "polib", specifier = ">=1.2.0" },
 | 
			
		||||
    { name = "pydantic", specifier = ">=2.11.7" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "typing-extensions"
 | 
			
		||||
version = "4.14.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "typing-inspection"
 | 
			
		||||
version = "0.4.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "typing-extensions" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										175
									
								
								scripts/validate_json.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								scripts/validate_json.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,175 @@
 | 
			
		||||
import enum
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import ast
 | 
			
		||||
from typing import Optional, Self
 | 
			
		||||
from pydantic import BaseModel, RootModel, Field, model_validator, ValidationError
 | 
			
		||||
import common
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_programmable_str(probe: str) -> None:
 | 
			
		||||
    try:
 | 
			
		||||
        ast.parse(probe)
 | 
			
		||||
    except SyntaxError:
 | 
			
		||||
        raise ValueError(
 | 
			
		||||
            f'String {probe} may not be a valid Python statement which is not suit for programmable field.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShowcaseType(enum.StrEnum):
 | 
			
		||||
    Nothing = 'none'
 | 
			
		||||
    Floor = 'floor'
 | 
			
		||||
    Rail = 'Rail'
 | 
			
		||||
    Wood = 'wood'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShowcaseCfgType(enum.StrEnum):
 | 
			
		||||
    Float = 'float'
 | 
			
		||||
    Int = 'int'
 | 
			
		||||
    Bool = 'bool'
 | 
			
		||||
    Face = 'face'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShowcaseCfg(BaseModel):
 | 
			
		||||
    field: str = Field(frozen=True, strict=True)
 | 
			
		||||
    type: ShowcaseCfgType = Field(frozen=True)
 | 
			
		||||
    title: str = Field(frozen=True, strict=True)
 | 
			
		||||
    desc: str = Field(frozen=True, strict=True)
 | 
			
		||||
    default: str = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_prog_field(self) -> Self:
 | 
			
		||||
        validate_programmable_str(self.default)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Showcase(BaseModel):
 | 
			
		||||
    title: str = Field(frozen=True, strict=True)
 | 
			
		||||
    icon: str = Field(frozen=True, strict=True)
 | 
			
		||||
    type: ShowcaseType = Field(frozen=True)
 | 
			
		||||
    cfgs: list[ShowcaseCfg] = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Param(BaseModel):
 | 
			
		||||
    field: str = Field(frozen=True, strict=True)
 | 
			
		||||
    data: str = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_prog_field(self) -> Self:
 | 
			
		||||
        validate_programmable_str(self.data)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Var(BaseModel):
 | 
			
		||||
    field: str = Field(frozen=True, strict=True)
 | 
			
		||||
    data: str = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_prog_field(self) -> Self:
 | 
			
		||||
        validate_programmable_str(self.data)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Vertex(BaseModel):
 | 
			
		||||
    skip: str = Field(frozen=True, strict=True)
 | 
			
		||||
    data: str = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_prog_field(self) -> Self:
 | 
			
		||||
        validate_programmable_str(self.skip)
 | 
			
		||||
        validate_programmable_str(self.data)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Face(BaseModel):
 | 
			
		||||
    skip: str = Field(frozen=True, strict=True)
 | 
			
		||||
    texture: str = Field(frozen=True, strict=True)
 | 
			
		||||
    indices: list[int] = Field(frozen=True, strict=True)
 | 
			
		||||
    uvs: list[str] = Field(frozen=True, strict=True)
 | 
			
		||||
    normals: Optional[list[str]] = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_count(self) -> Self:
 | 
			
		||||
        expected_count = len(self.indices)
 | 
			
		||||
        if len(self.uvs) != expected_count:
 | 
			
		||||
            raise ValueError('The length of uv array is not matched with indices.')
 | 
			
		||||
        if (self.normals is not None) and (len(self.normals) != expected_count):
 | 
			
		||||
            raise ValueError('The length of normal array is not matched with indices.')
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_prog_field(self) -> Self:
 | 
			
		||||
        validate_programmable_str(self.skip)
 | 
			
		||||
        validate_programmable_str(self.texture)
 | 
			
		||||
        for i in self.uvs:
 | 
			
		||||
            validate_programmable_str(i)
 | 
			
		||||
        if self.normals is not None:
 | 
			
		||||
            for i in self.normals:
 | 
			
		||||
                validate_programmable_str(i)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Instance(BaseModel):
 | 
			
		||||
    identifier: str = Field(frozen=True, strict=True)
 | 
			
		||||
    skip: str = Field(frozen=True, strict=True)
 | 
			
		||||
    params: dict[str, str] = Field(frozen=True, strict=True)
 | 
			
		||||
    transform: str = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_prog_field(self) -> Self:
 | 
			
		||||
        validate_programmable_str(self.skip)
 | 
			
		||||
        for v in self.params.values():
 | 
			
		||||
            validate_programmable_str(v)
 | 
			
		||||
        validate_programmable_str(self.transform)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
IDENTIFIERS: set[str] = set()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Prototype(BaseModel):
 | 
			
		||||
    identifier: str = Field(frozen=True, strict=True)
 | 
			
		||||
    showcase: Optional[Showcase] = Field(frozen=True, strict=True)
 | 
			
		||||
    params: list[Param] = Field(frozen=True, strict=True)
 | 
			
		||||
    skip: str = Field(frozen=True, strict=True)
 | 
			
		||||
    vars: list[Var] = Field(frozen=True, strict=True)
 | 
			
		||||
    vertices: list[Vertex] = Field(frozen=True, strict=True)
 | 
			
		||||
    faces: list[Face] = Field(frozen=True, strict=True)
 | 
			
		||||
    instances: list[Instance] = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_identifier(self) -> Self:
 | 
			
		||||
        global IDENTIFIERS
 | 
			
		||||
        if self.identifier in IDENTIFIERS:
 | 
			
		||||
            raise ValueError(f'Identifier {self.identifier} is already registered.')
 | 
			
		||||
        else:
 | 
			
		||||
            IDENTIFIERS.add(self.identifier)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    @model_validator(mode='after')
 | 
			
		||||
    def verify_prog_field(self) -> Self:
 | 
			
		||||
        validate_programmable_str(self.skip)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Prototypes(RootModel):
 | 
			
		||||
    root: list[Prototype] = Field(frozen=True, strict=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_json() -> None:
 | 
			
		||||
    raw_json_folder = common.get_plugin_folder() / 'raw_jsons'
 | 
			
		||||
 | 
			
		||||
    for json_file in raw_json_folder.rglob('*.json'):
 | 
			
		||||
        logging.info(f'Validating {json_file} ...')
 | 
			
		||||
        try:
 | 
			
		||||
            with open(json_file, 'r', encoding='utf-8') as f:
 | 
			
		||||
                docuement = json.load(f)
 | 
			
		||||
                Prototypes.model_validate(docuement)
 | 
			
		||||
        except json.JSONDecodeError as e:
 | 
			
		||||
            logging.error(f'Can not load file {json_file}. It may not a valid JSON file. Reason: {e}')
 | 
			
		||||
        except ValidationError as e:
 | 
			
		||||
            logging.error(f'File {json_file} is not correct. Reason: {e}')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    common.setup_logging()
 | 
			
		||||
    validate_json()
 | 
			
		||||
		Reference in New Issue
	
	Block a user