├── tests ├── __init__.py ├── cli │ ├── test.txt │ ├── confs │ │ ├── conf_local.py │ │ ├── conf_api_title_limit.py │ │ ├── conf_err_no_path.py │ │ ├── conf_api_err_invalid_params.py │ │ ├── conf_chars_to_omit.py │ │ ├── conf_api_list_categorymembers.py │ │ ├── conf_api_zhwiki.py │ │ ├── conf_api_params.py │ │ └── conf_api_continue.py │ └── test_conf.py ├── lib │ ├── test_bad_string.py │ ├── test_non_opencc_t2s.py │ ├── test_user_agent_setting.py │ ├── test_api_zhwiki.py │ ├── util.py │ ├── test_api_http_proxy.py │ ├── test_try_file.py │ ├── test_dict_name.py │ ├── test_build.py │ ├── test_log_level.py │ ├── test_fixfile.py │ ├── test_exporters_pypinyin.py │ └── test_utils.py └── opencc │ └── test_opencc.py ├── mw2fcitx ├── __init__.py ├── tweaks │ ├── __init__.py │ └── moegirl.py ├── exporters │ ├── __init__.py │ ├── opencc.py │ └── pypinyin.py ├── version.py ├── dictgen │ ├── __init__.py │ ├── pinyin.py │ └── rime.py ├── sample_fixfile.json ├── const.py ├── sample_config.py ├── logging.py ├── build_dict.py ├── main.py ├── utils.py ├── pipeline.py └── fetch.py ├── codecov.yml ├── pytest.ini ├── scripts └── test_version.sh ├── Makefile ├── .github └── workflows │ ├── trigger_build.yml │ ├── publish_package.yml │ ├── test.yml │ └── release.yml ├── LICENSE ├── BREAKING_CHANGES.md ├── .gitignore ├── README.md ├── pyproject.toml └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mw2fcitx/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mw2fcitx/tweaks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mw2fcitx/exporters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cli/test.txt: -------------------------------------------------------------------------------- 1 | 初音未来 2 | 迈克·杰克逊 -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - mw2fcitx/exports/opencc.py # deprecated 3 | -------------------------------------------------------------------------------- /mw2fcitx/version.py: -------------------------------------------------------------------------------- 1 | # Sync with pyproject.toml 2 | PKG_VERSION = "0.24.1" 3 | -------------------------------------------------------------------------------- /mw2fcitx/dictgen/__init__.py: -------------------------------------------------------------------------------- 1 | from .pinyin import gen as pinyin 2 | from .rime import gen as rime 3 | -------------------------------------------------------------------------------- /mw2fcitx/exporters/opencc.py: -------------------------------------------------------------------------------- 1 | # re-export for backward compatibility 2 | from .pypinyin import export 3 | -------------------------------------------------------------------------------- /mw2fcitx/sample_fixfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "朝之琉璃": "zhao'zhi'liu'li", 3 | "朝之瑠璃": "zhao'zhi'liu'li" 4 | } 5 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = test_*.py 3 | addopts =-slv --tb=short --cov=./ --cov-report=xml --cov-report=html --cov-report=term --cov-append 4 | norecursedirs = .git __pycache__ .venv -------------------------------------------------------------------------------- /scripts/test_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=$(grep PKG_VERSION mw2fcitx/version.py | sed -E 's/PKG_VERSION = "(.*)"/\1/g') 3 | echo "Expected version: $VERSION" 4 | grep "version = \"$VERSION\"" pyproject.toml && echo "OK!" || ( 5 | echo "BAD! Actual version: $(grep version pyproject.toml)" 6 | exit 1 7 | ) 8 | -------------------------------------------------------------------------------- /tests/lib/test_bad_string.py: -------------------------------------------------------------------------------- 1 | from mw2fcitx.pipeline import MWFPipeline 2 | 3 | 4 | def test_bad_string(): 5 | pipeline = MWFPipeline() 6 | pipeline.load_titles(["__INVALID__CHAR__"]) 7 | pipeline.convert_to_words([]) 8 | pipeline.export_words(converter="pypinyin") 9 | assert pipeline.exports == "" 10 | -------------------------------------------------------------------------------- /tests/lib/test_non_opencc_t2s.py: -------------------------------------------------------------------------------- 1 | from mw2fcitx.pipeline import MWFPipeline 2 | 3 | 4 | def test_non_opencc_t2s(): 5 | pipeline = MWFPipeline() 6 | pipeline.load_titles(["禮節"]) 7 | pipeline.convert_to_words([]) 8 | pipeline.export_words(converter="pypinyin") 9 | assert pipeline.exports == "禮節\tli'jie\t0\n" 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | help: 4 | @echo "test run test" 5 | 6 | build: 7 | poetry build 8 | 9 | test: 10 | poetry run pytest tests/ --ignore=tests/opencc 11 | 12 | test_opencc: 13 | poetry run pytest tests/opencc 14 | 15 | lint: 16 | poetry run pylint **/*.py 17 | 18 | format: 19 | poetry run autopep8 --in-place mw2fcitx/**/*.py 20 | 21 | test_version: 22 | @bash scripts/test_version.sh -------------------------------------------------------------------------------- /tests/lib/test_user_agent_setting.py: -------------------------------------------------------------------------------- 1 | import random 2 | from mw2fcitx.utils import create_requests_session 3 | 4 | 5 | def test_user_agent_setting(): 6 | rnd = random.random() 7 | user_agent = f"rnd/{rnd}" 8 | s = create_requests_session(user_agent) 9 | assert s.headers.get("User-Agent") == user_agent 10 | 11 | 12 | def test_null_user_agent_setting(): 13 | s = create_requests_session(None) 14 | assert "MW2Fcitx" in str(s.headers.get("User-Agent")) 15 | -------------------------------------------------------------------------------- /mw2fcitx/const.py: -------------------------------------------------------------------------------- 1 | LIBIME_BIN_NAME = "libime_pinyindict" 2 | LIBIME_REPOLOGY_URL = "https://repology.org/project/libime" 3 | PARTIAL_DEPRECATED_APCONTINUE = "apcontinue" 4 | PARTIAL_CONTINUE_DICT = "continue_dict" 5 | PARTIAL_TITLES = "titles" 6 | ADVANCED_MODE_TRIGGER_PARAMETER_NAMES = ["action", "list", "format"] 7 | LOG_LEVEL_ENV_VARIABLE = "LOG_LEVEL" 8 | 9 | # kwargs name 10 | PYPINYIN_KW_CHARACTERS_TO_OMIT = "characters_to_omit" 11 | PYPINYIN_KW_DISABLE_INSTINCT_PINYIN = "disable_instinct_pinyin" 12 | -------------------------------------------------------------------------------- /tests/lib/test_api_zhwiki.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from mw2fcitx.pipeline import MWFPipeline 4 | from mw2fcitx.tweaks.moegirl import tweaks as moegirl_tweaks 5 | from tests.lib.util import requires_real_world 6 | 7 | 8 | @requires_real_world() 9 | def test_pipeline_basic(): 10 | pipeline = MWFPipeline("https://zh.wikipedia.org/w/api.php") 11 | pipeline.fetch_titles(title_limit=50) 12 | pipeline.convert_to_words(moegirl_tweaks) 13 | pipeline.export_words() 14 | pipeline.generate_dict(generator="rime") 15 | -------------------------------------------------------------------------------- /tests/lib/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | REAL_WORLD_ENV_NAME = "TEST_REAL_WORLD" 5 | 6 | 7 | def requires_real_world(): 8 | real_world_test_flag = os.environ.get(REAL_WORLD_ENV_NAME) or "false" 9 | 10 | return pytest.mark.skipif( 11 | real_world_test_flag.lower() != "true", 12 | reason=f"Skipping real-world test as ${REAL_WORLD_ENV_NAME} is not `true`" 13 | ) 14 | 15 | 16 | def get_sorted_word_list(content: str) -> str: 17 | return "\n".join(sorted(content.split("\n")[5:])).strip() 18 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_local.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | from mw2fcitx.tweaks.moegirl import tweaks 3 | 4 | exports = { 5 | "source": { 6 | "file_path": "tests/cli/test.txt" 7 | }, 8 | "tweaks": 9 | tweaks, 10 | "converter": { 11 | "use": "opencc", 12 | "kwargs": {} 13 | }, 14 | "generator": [{ 15 | "use": "rime", 16 | "kwargs": { 17 | "name": "e2etest_local", 18 | "output": "test_local_result.dict.yml" 19 | } 20 | }] 21 | } 22 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_api_title_limit.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | 3 | exports = { 4 | "source": { 5 | "api_path": "https://zh.wikipedia.org/w/api.php", 6 | "kwargs": { 7 | "title_limit": 25, 8 | "api_params": { 9 | "aplimit": 12 10 | }, 11 | "output": "test_api_title_limit.titles.txt" 12 | } 13 | }, 14 | "tweaks": [], 15 | "converter": { 16 | "use": "opencc", 17 | "kwargs": {} 18 | }, 19 | "generator": [] 20 | } 21 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_err_no_path.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | from mw2fcitx.tweaks.moegirl import tweaks 3 | 4 | exports = { 5 | "source": { 6 | "kwargs": { 7 | "title_limit": 50, 8 | "output": "test_result.txt" 9 | } 10 | }, 11 | "tweaks": 12 | tweaks, 13 | "converter": { 14 | "use": "opencc", 15 | "kwargs": {} 16 | }, 17 | "generator": [{ 18 | "use": "rime", 19 | "kwargs": { 20 | "name": "err_local", 21 | "output": "err_local.dict.yml" 22 | } 23 | }] 24 | } 25 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_api_err_invalid_params.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | 3 | exports = { 4 | "source": { 5 | "api_path": "https://zh.wikipedia.org/w/api.php", 6 | "kwargs": { 7 | "title_limit": 10, 8 | "api_params": { 9 | "action": "paraminfo", 10 | "modules": "query+allpages" 11 | }, 12 | "output": "test_err_invalid_api_params.titles.txt" 13 | } 14 | }, 15 | "tweaks": [], 16 | "converter": { 17 | "use": "opencc", 18 | "kwargs": {} 19 | }, 20 | "generator": [] 21 | } 22 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_chars_to_omit.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | 3 | exports = { 4 | "source": { 5 | "file_path": "tests/cli/test.txt", 6 | "kwargs": { 7 | "title_limit": 50, 8 | "output": "test_result.txt" 9 | } 10 | }, 11 | "tweaks": [], 12 | "converter": { 13 | "use": "pypinyin", 14 | "kwargs": { 15 | "characters_to_omit": ["·"] 16 | } 17 | }, 18 | "generator": [{ 19 | "use": "rime", 20 | "kwargs": { 21 | "name": "e2etest_local", 22 | "output": "test_chars_to_omit.dict.yml" 23 | } 24 | }] 25 | } 26 | -------------------------------------------------------------------------------- /tests/opencc/test_opencc.py: -------------------------------------------------------------------------------- 1 | from mw2fcitx.pipeline import MWFPipeline 2 | from mw2fcitx.tweaks.moegirl import tweak_opencc_t2s 3 | 4 | 5 | def test_opencc_t2s(): 6 | pipeline = MWFPipeline() 7 | pipeline.load_titles(["禮節"]) 8 | pipeline.convert_to_words([tweak_opencc_t2s]) 9 | print(pipeline.titles) 10 | pipeline.export_words(converter="pypinyin") 11 | assert pipeline.exports == "礼节\tli'jie\t0\n" 12 | 13 | 14 | def test_dedup(): 15 | pipeline = MWFPipeline() 16 | pipeline.load_titles(["禮節", "礼节"]) 17 | pipeline.convert_to_words([tweak_opencc_t2s]) 18 | pipeline.export_words(converter="pypinyin") 19 | assert pipeline.exports == "礼节\tli'jie\t0\n" 20 | -------------------------------------------------------------------------------- /tests/lib/test_api_http_proxy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from requests.exceptions import ProxyError 4 | 5 | from tests.lib.util import requires_real_world 6 | 7 | 8 | @requires_real_world() 9 | def test_http_proxy(): 10 | old_value = os.environ.get("HTTPS_PROXY") 11 | 12 | os.environ["HTTPS_PROXY"] = "http://127.0.0.1:39999" 13 | from mw2fcitx.pipeline import MWFPipeline 14 | pipeline = MWFPipeline("https://zh.wikipedia.org/w/api.php") 15 | with pytest.raises(ProxyError): 16 | pipeline.fetch_titles(title_limit=50) 17 | 18 | if old_value is not None: 19 | os.environ["HTTPS_PROXY"] = old_value 20 | else: 21 | del os.environ["HTTPS_PROXY"] 22 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_api_list_categorymembers.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | 3 | exports = { 4 | "source": { 5 | "api_path": "https://zh.wikipedia.org/w/api.php", 6 | "kwargs": { 7 | "title_limit": 10, 8 | "api_params": { 9 | "action": "query", 10 | "list": "categorymembers", 11 | "cmtitle": "Category:天津市历史风貌建筑", 12 | "cmlimit": 5 13 | }, 14 | "output": "test_list_categorymembers.titles.txt" 15 | } 16 | }, 17 | "tweaks": [], 18 | "converter": { 19 | "use": "opencc", 20 | "kwargs": {} 21 | }, 22 | "generator": [] 23 | } 24 | -------------------------------------------------------------------------------- /tests/lib/test_try_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import tempfile 4 | 5 | from mw2fcitx.utils import try_file 6 | 7 | 8 | def test_invalid_py_file(): 9 | tmpfile = tempfile.NamedTemporaryFile() 10 | tmpfile.write(b"invalid") 11 | tmpfile.close() 12 | assert try_file(tmpfile.name) is False 13 | 14 | 15 | def test_unreadable_file(): 16 | rnd_filename = f"{random.random()}.rnd" 17 | for i in range(3): 18 | if not os.access(rnd_filename, os.R_OK): 19 | break 20 | if i == 2: 21 | # Let's just return if I cannot find a unreadable file 22 | return 23 | rnd_filename = f"{random.random()}.rnd" 24 | assert try_file(rnd_filename) is False 25 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_api_zhwiki.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | from mw2fcitx.tweaks.moegirl import tweaks 3 | 4 | exports = { 5 | "source": { 6 | "api_path": "https://zh.wikipedia.org/w/api.php", 7 | "kwargs": { 8 | "title_limit": 50, 9 | "output": "titles.txt" 10 | } 11 | }, 12 | "tweaks": 13 | tweaks, 14 | "converter": { 15 | "use": "opencc", 16 | "kwargs": {} 17 | }, 18 | "generator": [{ 19 | "use": "rime", 20 | "kwargs": { 21 | "output": "moegirl.dict.yml" 22 | } 23 | }, { 24 | "use": "pinyin", 25 | "kwargs": { 26 | "output": "moegirl.dict" 27 | } 28 | }] 29 | } 30 | -------------------------------------------------------------------------------- /mw2fcitx/dictgen/pinyin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import tempfile 3 | import subprocess 4 | 5 | from ..const import LIBIME_BIN_NAME, LIBIME_REPOLOGY_URL 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | def gen(text, **kwargs): 11 | with tempfile.NamedTemporaryFile("w+") as file: 12 | file.write(text) 13 | log.info(f"Running {LIBIME_BIN_NAME}...") 14 | try: 15 | subprocess.run([LIBIME_BIN_NAME, file.name, kwargs["output"]], 16 | check=True) 17 | except FileNotFoundError: 18 | log.error( 19 | f"The program \"{LIBIME_BIN_NAME}\" is not found. " 20 | f"Please install libime: {LIBIME_REPOLOGY_URL}" 21 | ) 22 | raise 23 | log.info("Dictionary generated.") 24 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_api_params.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | from mw2fcitx.tweaks.moegirl import tweaks 3 | 4 | exports = { 5 | "source": { 6 | "api_path": "https://zh.wikipedia.org/w/api.php", 7 | "kwargs": { 8 | "title_limit": 20, 9 | "api_params": { 10 | "apnamespace": 4, 11 | "apprefix": "《求闻》/2019年第1卷" 12 | }, 13 | "output": "titles.txt" 14 | } 15 | }, 16 | "tweaks": 17 | tweaks, 18 | "converter": { 19 | "use": "opencc", 20 | "kwargs": {} 21 | }, 22 | "generator": [{ 23 | "use": "rime", 24 | "kwargs": { 25 | "name": "e2etest_local", 26 | "output": "test_api_params.dict.yml" 27 | } 28 | }] 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/trigger_build.yml: -------------------------------------------------------------------------------- 1 | name: "[moegirl] Trigger Automatic Dictionary Build" 2 | 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: "37 19 9 * *" 7 | 8 | jobs: 9 | trigger-bnp: 10 | name: Trigger build & publish 11 | permissions: 12 | actions: write 13 | contents: read 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 17 | with: 18 | script: | 19 | github.request('POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches', { 20 | owner: context.repo.owner, 21 | repo: context.repo.repo, 22 | workflow_id: "build_dict.yml", 23 | ref: 'pkg-moegirl' 24 | }) 25 | debug: true 26 | -------------------------------------------------------------------------------- /mw2fcitx/dictgen/rime.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import yaml 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | 8 | def gen(text, **kwargs): 9 | name = kwargs.get("name") or "unnamed_dict" 10 | version = kwargs.get("version") or "0.1" 11 | sort = kwargs.get("sort") or "by_weight" 12 | text = re.sub(r'[ ][ ]*', '\t', text) 13 | text = text.replace("\t0", "") 14 | text = text.replace("'", " ") 15 | header = yaml.dump({ 16 | "name": name, 17 | "version": version, 18 | "sort": sort 19 | }) 20 | text = f'---\n{header.strip()}\n...\n' + text 21 | output_path = kwargs.get("output") 22 | if output_path is not None: 23 | with open(output_path, "w", encoding="utf-8") as file: 24 | file.write(text) 25 | else: 26 | print(text) 27 | log.info("Dictionary generated.") 28 | return text 29 | -------------------------------------------------------------------------------- /mw2fcitx/sample_config.py: -------------------------------------------------------------------------------- 1 | from .tweaks.moegirl import tweaks 2 | 3 | exports = { 4 | "source": { 5 | "api_path": "https://zh.wikipedia.org/w/api.php", 6 | "kwargs": { 7 | "api_title_limit": 120, 8 | "file_title_limit": 60, 9 | "title_limit": 240, 10 | "partial": "partial.json", 11 | "output": "titles.txt" 12 | } 13 | }, 14 | "tweaks": 15 | tweaks, 16 | "converter": { 17 | "use": "pypinyin", 18 | "kwargs": { 19 | "fixfile": "sample_fixfile.json" 20 | } 21 | }, 22 | "generator": [{ 23 | "use": "rime", 24 | "kwargs": { 25 | "output": "moegirl.dict.yml" 26 | } 27 | }, { 28 | "use": "pinyin", 29 | "kwargs": { 30 | "output": "moegirl.dict" 31 | } 32 | }] 33 | } 34 | -------------------------------------------------------------------------------- /tests/cli/confs/conf_api_continue.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | from mw2fcitx.tweaks.moegirl import tweaks 3 | 4 | exports = { 5 | "source": { 6 | "api_path": "https://zh.wikipedia.org/w/api.php", 7 | "kwargs": { 8 | "request_delay": 2, 9 | "title_limit": 5, # to test the paginator 10 | "api_params": { 11 | "aplimit": 1, 12 | }, 13 | "output": "titles.txt" 14 | } 15 | }, 16 | "tweaks": 17 | tweaks, 18 | "converter": { 19 | "use": "opencc", 20 | "kwargs": {} 21 | }, 22 | "generator": [{ 23 | "use": "rime", 24 | "kwargs": { 25 | "output": "moegirl.dict.yml" 26 | } 27 | }, { 28 | "use": "pinyin", 29 | "kwargs": { 30 | "output": "moegirl.dict" 31 | } 32 | }] 33 | } 34 | -------------------------------------------------------------------------------- /tests/lib/test_dict_name.py: -------------------------------------------------------------------------------- 1 | from mw2fcitx.pipeline import MWFPipeline 2 | 3 | 4 | def test_rime_dict_name(): 5 | pipeline = MWFPipeline() 6 | pipeline.load_titles(["测试"]) 7 | pipeline.convert_to_words([]) 8 | pipeline.export_words(converter="pypinyin") 9 | pipeline.generate_dict(generator="rime", **{ 10 | "name": "new_name", 11 | "version": "1.3.5" 12 | }) 13 | assert pipeline.dict.strip() == """ 14 | --- 15 | name: new_name 16 | sort: by_weight 17 | version: 1.3.5 18 | ... 19 | 测试 ce shi 20 | """.strip() 21 | 22 | 23 | def test_rime_dict_default_name(): 24 | pipeline = MWFPipeline() 25 | pipeline.load_titles(["测试"]) 26 | pipeline.convert_to_words([]) 27 | pipeline.export_words(converter="pypinyin") 28 | pipeline.generate_dict(generator="rime") 29 | assert pipeline.dict.strip() == """ 30 | --- 31 | name: unnamed_dict 32 | sort: by_weight 33 | version: '0.1' 34 | ... 35 | 测试 ce shi 36 | """.strip() 37 | -------------------------------------------------------------------------------- /tests/lib/test_build.py: -------------------------------------------------------------------------------- 1 | from os.path import getsize 2 | 3 | from mw2fcitx.pipeline import MWFPipeline 4 | 5 | 6 | def test_pipeline_basic(): 7 | pipeline = MWFPipeline() 8 | pipeline.load_titles(["测试", "百科", "朝之琉璃"]) 9 | pipeline.convert_to_words([]) 10 | pipeline.export_words(converter="opencc", 11 | fixfile="mw2fcitx/sample_fixfile.json") 12 | assert pipeline.exports != "" 13 | pipeline.generate_dict(generator="rime", 14 | output="test.dict.yml", 15 | name="test", 16 | version="1.0") 17 | assert getsize("test.dict.yml") > 0 18 | pipeline.generate_dict( 19 | generator="pinyin", 20 | output="test.dict", 21 | ) 22 | assert getsize("test.dict") > 0 23 | # pylint: disable=consider-using-with 24 | assert ("朝之琉璃 zhao zhi liu li" in open('test.dict.yml', 25 | "r", 26 | encoding='utf-8').read()) 27 | -------------------------------------------------------------------------------- /tests/lib/test_log_level.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import pytest 5 | from mw2fcitx.logging import setup_logger, update_log_level, DEFAULT_LOG_LEVEL 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def reset_logging(): 10 | yield 11 | if os.environ.get("LOG_LEVEL") is not None: 12 | del os.environ["LOG_LEVEL"] 13 | logging.getLogger().setLevel(logging.NOTSET) 14 | 15 | 16 | def test_args_good(): 17 | update_log_level("CRITICAL") 18 | assert ( 19 | logging.getLogger().level == logging.CRITICAL 20 | ) 21 | 22 | 23 | def test_args_bad(): 24 | update_log_level("NOTGOOD") 25 | assert ( 26 | logging.getLogger().level == logging.NOTSET 27 | ) 28 | 29 | 30 | def test_envvar_override(): 31 | os.environ["LOG_LEVEL"] = "CRITICAL" 32 | update_log_level("WARNING") 33 | assert ( 34 | logging.getLogger().level == logging.CRITICAL 35 | ) 36 | 37 | 38 | def test_envvar_bad(): 39 | os.environ["LOG_LEVEL"] = "NOTGOOD" 40 | update_log_level("WARNING") 41 | assert ( 42 | logging.getLogger().level == logging.WARNING 43 | ) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Outvi V 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/lib/test_fixfile.py: -------------------------------------------------------------------------------- 1 | from mw2fcitx.pipeline import MWFPipeline 2 | from tests.lib.util import get_sorted_word_list 3 | 4 | 5 | def test_fixfile_with_partial(): 6 | pipeline = MWFPipeline() 7 | pipeline.load_titles(["测试", "测试二", "刻俄柏", "刻俄柏的灰蕈迷境"]) 8 | pipeline.convert_to_words([]) 9 | pipeline.export_words(converter="pypinyin", fix_table={ 10 | "测试": "pin'yin", 11 | "测试二": "yi'er'san", 12 | "刻俄柏": "ke'e'bo", 13 | }) 14 | pipeline.generate_dict(generator="rime") 15 | assert get_sorted_word_list(pipeline.dict) == """ 16 | 刻俄柏 ke e bo 17 | 刻俄柏的灰蕈迷境 ke e bo de hui xun mi jing 18 | 测试 pin yin 19 | 测试二 yi er san 20 | """.strip() 21 | 22 | 23 | def test_fixfile_no_partial(): 24 | pipeline = MWFPipeline() 25 | pipeline.load_titles(["测试"]) 26 | pipeline.convert_to_words([]) 27 | pipeline.export_words(converter="pypinyin", fix_table={ 28 | "测试": "pin'yin" 29 | }) 30 | pipeline.generate_dict(generator="rime") 31 | assert pipeline.dict.strip() == """ 32 | --- 33 | name: unnamed_dict 34 | sort: by_weight 35 | version: '0.1' 36 | ... 37 | 测试 pin yin 38 | """.strip() 39 | -------------------------------------------------------------------------------- /tests/lib/test_exporters_pypinyin.py: -------------------------------------------------------------------------------- 1 | from mw2fcitx.exporters.pypinyin import export 2 | 3 | 4 | def test_pypinyin_exporter(): 5 | assert ( 6 | export(["测试"]) == "测试\tce'shi\t0\n" 7 | ) 8 | 9 | assert ( 10 | export([ 11 | "测试", 12 | "琴吹䌷" # outloudvi/mw2fcitx#16 13 | ]) == "测试\tce'shi\t0\n" 14 | "琴吹䌷\tqin'chui'chou\t0\n" 15 | ) 16 | 17 | assert ( 18 | export([ 19 | "测试", 20 | "无效:词条" 21 | ]) == "测试\tce'shi\t0\n" 22 | ) 23 | 24 | 25 | def test_pypinyin_instinct_pinyin(): # outloudvi/mw2fcitx#29 26 | assert ( 27 | export([ 28 | "唔呣", 29 | "嗯啊啊" 30 | ]) == "唔呣\twu'mu\t0\n" 31 | "嗯啊啊\ten'a'a\t0\n" 32 | ) 33 | 34 | assert ( 35 | export([ 36 | "唔呣", 37 | "嗯啊啊" 38 | ], disable_instinct_pinyin=True 39 | ) == "唔呣\twu'm\t0\n" 40 | "嗯啊啊\tn'a'a\t0\n" 41 | ) 42 | 43 | 44 | def test_fixfile(): 45 | assert ( 46 | export([ 47 | "测试", 48 | "文档" 49 | ], fix_table={ 50 | "测试": "a'a" 51 | } 52 | ) == "测试\ta'a\t0\n" 53 | "文档\twen'dang\t0\n" 54 | ) 55 | -------------------------------------------------------------------------------- /.github/workflows/publish_package.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - master 8 | types: 9 | - closed 10 | 11 | jobs: 12 | pypi-publish: 13 | if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')) 14 | name: Upload release to PyPI 15 | runs-on: ubuntu-latest 16 | environment: 17 | name: pypi 18 | url: https://pypi.org/p/mw2fcitx 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | 24 | - name: Install Poetry 25 | run: pipx install poetry 26 | 27 | - name: Set up Python 3.12 28 | uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 29 | with: 30 | python-version: "3.12" 31 | cache: "poetry" 32 | 33 | - name: Install dependencies 34 | run: | 35 | poetry install 36 | poetry build 37 | 38 | - name: Publish package distributions to PyPI 39 | uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 40 | -------------------------------------------------------------------------------- /mw2fcitx/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from mw2fcitx.const import LOG_LEVEL_ENV_VARIABLE 5 | 6 | 7 | LOG_LEVEL_MAPPING = { 8 | "DEBUG": logging.DEBUG, 9 | "INFO": logging.INFO, 10 | "WARNING": logging.WARNING, 11 | "ERROR": logging.ERROR, 12 | "CRITICAL": logging.CRITICAL 13 | } 14 | 15 | DEFAULT_LOG_LEVEL_STRING = "DEBUG" 16 | DEFAULT_LOG_LEVEL = LOG_LEVEL_MAPPING[DEFAULT_LOG_LEVEL_STRING] 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | def setup_logger(): 22 | logging.basicConfig( 23 | level=DEFAULT_LOG_LEVEL, 24 | format='%(asctime)s %(name)s %(levelname)s - %(message)s', 25 | ) 26 | 27 | 28 | def update_log_level(args_log_level_str: str): 29 | override_log_level = LOG_LEVEL_MAPPING.get(args_log_level_str) 30 | if override_log_level is None: 31 | log.warning("Invalid --log-level: %s, ignoring", args_log_level_str) 32 | env_log_level_str = os.environ.get(LOG_LEVEL_ENV_VARIABLE) 33 | if env_log_level_str is not None: 34 | env_override_log_level = LOG_LEVEL_MAPPING.get(env_log_level_str) 35 | if env_override_log_level is None: 36 | log.warning( 37 | "Invalid environment variable `%s`: %s, ignoring", 38 | LOG_LEVEL_ENV_VARIABLE, 39 | env_log_level_str) 40 | else: 41 | override_log_level = env_override_log_level 42 | if override_log_level is not None: 43 | logging.getLogger().setLevel(override_log_level) 44 | -------------------------------------------------------------------------------- /BREAKING_CHANGES.md: -------------------------------------------------------------------------------- 1 | ## Breaking Changes 2 | 3 | ### 0.22.0 4 | 5 | * BREAKING: `fixfile` is now sent into `pypinyin` via [`load_phrases_dict`](https://pypinyin.readthedocs.io/zh-cn/master/api.html#pypinyin.load_phrases_dict). It may lead to different behaviors as before: In the past only the single word that fully match the phrase is affected, while an `fixfile` entry in the current design will also impact words that contain the phrase. 6 | 7 | ### 0.20.0 8 | 9 | * BREAKING: Like what `file_title_limit` is already doing, `api_title_limit` now also works to trim the title count to the exact limit number. 10 | * BREAKING: The exporter `opencc` is renamed to `pypinyin`. OpenCC-related Traditional/Simplified Chinese conversion is moved to be the `tweak_opencc_t2s` tweak. As a result, `mw2fcitx` will not have a hard dependency on [`opencc`](https://pypi.org/project/OpenCC/). If you need `tweak_opencc_t2s`, you may want to install `mw2fcitx[opencc]` which includes the `opencc` dependency. 11 | * BREAKING: As a result of the change listed above, `tweaks` in `mw2fcitx.tweaks.moegirl` no longer does automatic Traditional/Simplified Chinese conversion. 12 | * Switched to MIT License. 13 | 14 | ### 0.19.0 15 | 16 | * BREAKING: `source.kwargs.aplimit` is moved to `source.kwargs.api_params.aplimit`. 17 | 18 | ### 0.17.0 19 | 20 | * BREAKING: Pinyin "m" will be replaced to "mu" and "n" to "en". To revert the behavior, set `"disable_instinct_pinyin": False` for OpenCC converter. [[#29](https://github.com/outloudvi/mw2fcitx/issues/29)] -------------------------------------------------------------------------------- /mw2fcitx/build_dict.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | 4 | from .pipeline import MWFPipeline 5 | 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | def build(config): 11 | config["source"] = config["source"] or {} 12 | config["tweaks"] = config["tweaks"] or [] 13 | config["converter"] = config["converter"] or {} 14 | config["generator"] = config["generator"] or [] 15 | pipeline = MWFPipeline(config["source"].get("api_path")) 16 | has_contents = False 17 | if config["source"].get("api_path") is not None: 18 | has_contents = True 19 | pipeline.fetch_titles(**config["source"].get("kwargs")) 20 | if config["source"].get("file_path") is not None: 21 | has_contents = True 22 | title_file_path = config["source"].get("file_path") 23 | for i in title_file_path: 24 | source_kwargs = config["source"].get("kwargs") 25 | if source_kwargs is None: 26 | log.warning("source.kwargs does not exist; assuming null") 27 | source_kwargs = {} 28 | pipeline.load_titles_from_file(i, **source_kwargs) 29 | if not has_contents: 30 | log.error("No api_path or file_path provided. Stop.") 31 | sys.exit(1) 32 | pipeline.convert_to_words(config["tweaks"]) 33 | pipeline.export_words(config["converter"].get("use"), 34 | **config["converter"].get("kwargs")) 35 | generators = config["generator"] 36 | for gen in generators: 37 | pipeline.generate_dict(gen.get("use"), **gen.get("kwargs")) 38 | return pipeline.dict 39 | -------------------------------------------------------------------------------- /mw2fcitx/exporters/pypinyin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | from pypinyin import lazy_pinyin, load_phrases_dict 4 | 5 | from ..const import PYPINYIN_KW_CHARACTERS_TO_OMIT, PYPINYIN_KW_DISABLE_INSTINCT_PINYIN 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | DEFAULT_PLACEHOLDER = "_ERROR_" 10 | 11 | # https://github.com/outloudvi/mw2fcitx/issues/29 12 | INSTINCT_PINYIN_MAPPING = { 13 | "n": "en", 14 | "m": "mu", 15 | } 16 | 17 | 18 | def load_phrases(fix_table: dict): 19 | items = {} 20 | for (key, value) in fix_table.items(): 21 | phrases = list(map(lambda x: [x], value.split("'"))) 22 | items[key] = phrases 23 | load_phrases_dict(items) 24 | 25 | 26 | def export(words: List[str], **kwargs) -> str: 27 | disable_instinct_pinyin = kwargs.get( 28 | PYPINYIN_KW_DISABLE_INSTINCT_PINYIN) is True 29 | characters_to_omit = kwargs.get(PYPINYIN_KW_CHARACTERS_TO_OMIT, []) 30 | 31 | fix_table = kwargs.get("fix_table") or {} 32 | load_phrases(fix_table) 33 | 34 | result = "" 35 | count = 0 36 | for line in words: 37 | line = line.rstrip("\n") 38 | 39 | pinyin = None 40 | 41 | line_for_pinyin = line 42 | if len(characters_to_omit) > 0: 43 | line_for_pinyin = ''.join( 44 | [char for char in line_for_pinyin if char not in characters_to_omit]) 45 | 46 | if pinyin is None: 47 | pinyins = lazy_pinyin( 48 | line_for_pinyin, errors=lambda x: DEFAULT_PLACEHOLDER) 49 | if not disable_instinct_pinyin: 50 | pinyins = [INSTINCT_PINYIN_MAPPING.get(x, x) for x in pinyins] 51 | if DEFAULT_PLACEHOLDER in pinyins: 52 | # The word is not fully converable 53 | continue 54 | pinyin = "'".join(pinyins) 55 | if pinyin == line: 56 | # print("Failed to convert, ignoring:", pinyin, file=sys.stderr) 57 | continue 58 | 59 | result += "\t".join((line, pinyin, "0")) 60 | result += "\n" 61 | count += 1 62 | if count % 1000 == 0: 63 | log.debug("%d converted", count) 64 | 65 | if count % 1000 != 0 or count == 0: 66 | log.debug("%d converted", count) 67 | return result 68 | -------------------------------------------------------------------------------- /tests/cli/test_conf.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | 4 | from mw2fcitx.main import inner_main 5 | from tests.lib.util import requires_real_world, get_sorted_word_list 6 | 7 | 8 | def test_inner_main_name_without_py(): 9 | inner_main(['-c', 'tests/cli/confs/conf_local']) 10 | 11 | 12 | def test_inner_main_name_with_py(): 13 | inner_main(['-c', 'tests/cli/confs/conf_local.py']) 14 | 15 | 16 | def test_local(): 17 | inner_main(['-c', 'tests/cli/confs/conf_local']) 18 | with open("test_local_result.dict.yml", "r", encoding="utf-8") as f: 19 | assert get_sorted_word_list(f.read()) == """ 20 | 初音未来 chu yin wei lai 21 | 迈克杰克逊 mai ke jie ke xun""".strip() 22 | 23 | 24 | def test_chars_to_omit(): 25 | inner_main(['-c', 'tests/cli/confs/conf_chars_to_omit']) 26 | with open("test_chars_to_omit.dict.yml", "r", encoding="utf-8") as f: 27 | assert get_sorted_word_list(f.read()) == """ 28 | 初音未来 chu yin wei lai 29 | 迈克·杰克逊 mai ke jie ke xun""".strip() 30 | 31 | 32 | def test_err_no_path(): 33 | with pytest.raises(SystemExit): 34 | inner_main(['-c', 'tests/cli/confs/conf_err_no_path']) 35 | 36 | 37 | @requires_real_world() 38 | def test_api_err_invalid_params(): 39 | with pytest.raises(SystemExit): 40 | inner_main(['-c', 'tests/cli/confs/conf_api_err_invalid_params']) 41 | 42 | 43 | @requires_real_world() 44 | def test_api_continue(): 45 | # this should run at least 8 secs = 2 * (5 / 1) - 2 46 | start = time.perf_counter() 47 | inner_main(['-c', 'tests/cli/confs/conf_api_continue']) 48 | end = time.perf_counter() 49 | assert end-start > 8 50 | 51 | 52 | @requires_real_world() 53 | def test_api_params(): 54 | inner_main(['-c', 'tests/cli/confs/conf_api_params']) 55 | with open("test_api_params.dict.yml", "r", encoding="utf-8") as f: 56 | assert get_sorted_word_list(f.read()) == """ 57 | 专题关注 zhuan ti guan zhu 58 | 全域动态 quan yu dong tai 59 | 本地社群新闻 ben di she qun xin wen""".strip() 60 | 61 | 62 | @requires_real_world() 63 | def test_api_title_limit(): 64 | inner_main(['-c', 'tests/cli/confs/conf_api_title_limit']) 65 | with open("test_api_title_limit.titles.txt", "r", encoding="utf-8") as f: 66 | assert len(f.read().split("\n")) == 25 67 | 68 | 69 | @requires_real_world() 70 | def test_api_list_categorymembers(): 71 | inner_main(['-c', 'tests/cli/confs/conf_api_list_categorymembers']) 72 | with open("test_list_categorymembers.titles.txt", "r", encoding="utf-8") as f: 73 | assert len(f.read().split("\n")) == 10 74 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: ["master"] 9 | pull_request: 10 | branches: ["master"] 11 | 12 | permissions: 13 | contents: read 14 | id-token: write 15 | 16 | jobs: 17 | lint: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | 22 | - name: Install Poetry 23 | run: pipx install poetry 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | cache: "poetry" 30 | 31 | - name: Run pylint 32 | run: | 33 | poetry install 34 | make lint 35 | 36 | - name: Test version mark 37 | run: make test_version 38 | 39 | buildtest: 40 | strategy: 41 | matrix: 42 | pyversion: 43 | - "3.9" 44 | - "3.10" # YAML trick: add "" especially between 3.10 45 | - "3.11" 46 | - "3.12" 47 | - "3.13" 48 | runs-on: ubuntu-latest 49 | needs: lint 50 | steps: 51 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 52 | 53 | - name: Install libime 54 | run: sudo apt install -y libime-bin 55 | 56 | - name: Install Poetry 57 | run: pipx install poetry 58 | 59 | - name: Set up Python ${{ matrix.pyversion }} 60 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 61 | with: 62 | python-version: ${{ matrix.pyversion }} 63 | cache: "poetry" 64 | 65 | - name: Install dependencies 66 | run: | 67 | poetry install 68 | poetry env info 69 | poetry show 70 | 71 | - name: Test with pytest 72 | env: 73 | TEST_REAL_WORLD: true 74 | run: make test 75 | 76 | - name: Run opencc-related tests 77 | env: 78 | TEST_REAL_WORLD: true 79 | run: | 80 | poetry install -E opencc 81 | make test_opencc 82 | 83 | - name: Upload coverage reports to Codecov 84 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 85 | with: 86 | token: ${{ secrets.CODECOV_REPO_TOKEN }} 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | /lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | /*.raw 141 | *.dict 142 | *.dict.yaml 143 | *.dict.yml 144 | *titles.txt 145 | /.vscode 146 | -------------------------------------------------------------------------------- /tests/lib/test_utils.py: -------------------------------------------------------------------------------- 1 | from mw2fcitx.utils import is_libime_used, sanitize, smart_rewrite 2 | 3 | 4 | def test_is_libime_used(): 5 | assert ( 6 | is_libime_used({}) is False 7 | ) 8 | 9 | assert ( 10 | is_libime_used({ 11 | "generator": [{ 12 | "use": "rime", 13 | "kwargs": { 14 | "output": "1.yml" 15 | } 16 | }] 17 | }) is False 18 | ) 19 | 20 | assert ( 21 | is_libime_used({ 22 | "generator": [{ 23 | "use": "rime", 24 | "kwargs": { 25 | "output": "1.yml" 26 | } 27 | }, { 28 | "use": "pinyin", 29 | "kwargs": { 30 | "output": "1.dict" 31 | } 32 | }] 33 | }) is True 34 | ) 35 | 36 | 37 | def test_sanitize(): 38 | def test(): 39 | pass 40 | assert ( 41 | sanitize(test) == "[func test]" 42 | ) 43 | 44 | assert sanitize(lambda x: x) == "[func ]" 45 | 46 | assert (sanitize({ 47 | "a": [1, "b"], 48 | "c": { 49 | "d": None 50 | } 51 | }) == { 52 | "a": ['1', "b"], 53 | "c": { 54 | "d": "[]" 55 | } 56 | }) 57 | 58 | 59 | def test_smart_rewrite(): 60 | assert ( 61 | smart_rewrite( 62 | { 63 | "generator": [], 64 | "source": { 65 | "file_path": [] 66 | } 67 | } 68 | ) == { 69 | "generator": [], 70 | "source": { 71 | "file_path": [] 72 | } 73 | } 74 | ) 75 | 76 | assert ( 77 | smart_rewrite( 78 | { 79 | "generator": { 80 | "use": "rime", 81 | "kwargs": { 82 | "output": "moegirl.dict.yml" 83 | } 84 | }, 85 | "source": { 86 | "file_path": [] 87 | } 88 | } 89 | ) == { 90 | "generator": [{ 91 | "use": "rime", 92 | "kwargs": { 93 | "output": "moegirl.dict.yml" 94 | } 95 | }], 96 | "source": { 97 | "file_path": [] 98 | } 99 | } 100 | ) 101 | 102 | assert ( 103 | smart_rewrite( 104 | { 105 | "generator": [], 106 | "source": { 107 | "file_path": "1.txt" 108 | } 109 | } 110 | ) == { 111 | "generator": [], 112 | "source": { 113 | "file_path": ["1.txt"] 114 | } 115 | } 116 | ) 117 | -------------------------------------------------------------------------------- /mw2fcitx/tweaks/moegirl.py: -------------------------------------------------------------------------------- 1 | # This collation file is for moegirl.org. 2 | # It MIGHT NOT be fit for other wikis. 3 | from typing import List 4 | from ..utils import normalize 5 | 6 | 7 | def dont_have(string: str, array: List[str]): 8 | for i in array: 9 | if string.find(i) != -1: 10 | return False 11 | return True 12 | 13 | 14 | def split_and_merge_single(group: List[str], spliter: str): 15 | ret = [] 16 | for i in group: 17 | for j in i.split(spliter): 18 | ret.append(j) 19 | return ret 20 | 21 | 22 | def tweak_remove_char(char): 23 | 24 | def cb(words): 25 | return list(map(lambda x: x.replace(char, ""), words)) 26 | 27 | return cb 28 | 29 | 30 | def tweak_len_more_than(length): 31 | 32 | def cb(words): 33 | return list(filter(lambda x: len(x) > length, words)) 34 | 35 | return cb 36 | 37 | 38 | def tweak_remove_word_includes(items): 39 | 40 | def cb(words): 41 | return list(filter(lambda x: dont_have(x, items), words)) 42 | 43 | return cb 44 | 45 | 46 | def tweak_split_word_with(spliters): 47 | 48 | def cb(items: List[str]): 49 | ret = items 50 | for i in spliters: 51 | tmp = [] 52 | for j in split_and_merge_single(ret, i): 53 | tmp.append(j) 54 | ret = tmp 55 | return ret 56 | 57 | return cb 58 | 59 | 60 | def tweak_trim_suffix(suffixes): 61 | 62 | def cb(items: List[str]): 63 | ret = [] 64 | for i in items: 65 | for j in suffixes: 66 | i = i.removesuffix(j) 67 | ret.append(i) 68 | return ret 69 | 70 | return cb 71 | 72 | 73 | def tweak_remove_regex(regexes): 74 | # Don't introduce extra import in public configuration files 75 | # pylint: disable=import-outside-toplevel 76 | from re import compile as regex_compile 77 | compiled_regexes = list(map(regex_compile, regexes)) 78 | 79 | def cb(items: List[str]): 80 | ret = items 81 | 82 | for rgx in compiled_regexes: 83 | ret = filter(lambda x, rgx=rgx: not rgx.match(x), ret) 84 | return list(ret) 85 | 86 | return cb 87 | 88 | 89 | def tweak_normalize(words): 90 | ret = [] 91 | for i in words: 92 | ret.append(normalize(i)) 93 | return ret 94 | 95 | 96 | def tweak_opencc_t2s(words): 97 | import opencc 98 | converter = opencc.OpenCC('t2s.json') 99 | ret = [] 100 | for i in words: 101 | ret.append(converter.convert(i)) 102 | return ret 103 | 104 | 105 | tweaks = [ 106 | tweak_remove_word_includes(["○", "〇"]), 107 | tweak_split_word_with( 108 | [":", "/", "(", ")", "(", ")", "【", "】", "『", "』", "/", " ", "!", "!"]), 109 | tweak_len_more_than(1), 110 | tweak_remove_char("·"), 111 | tweak_trim_suffix(["系列", "列表", "对照表"]), 112 | tweak_remove_regex(["^第.*(次|话)$"]), 113 | tweak_normalize, 114 | ] 115 | -------------------------------------------------------------------------------- /mw2fcitx/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import shutil 4 | import sys 5 | from argparse import ArgumentParser 6 | 7 | from .build_dict import build 8 | from .const import LIBIME_BIN_NAME, LIBIME_REPOLOGY_URL 9 | from .logging import DEFAULT_LOG_LEVEL_STRING, LOG_LEVEL_MAPPING, setup_logger, update_log_level 10 | from .utils import sanitize, is_libime_used, smart_rewrite, try_file 11 | 12 | 13 | def get_args(args): 14 | parser = ArgumentParser( 15 | usage="Fetch titles from online and generate a dictionary.") 16 | parser.add_argument("-c", 17 | "--config", 18 | dest="config", 19 | default="config.py", 20 | help="configuration file location") 21 | parser.add_argument("-n", 22 | "--name", 23 | dest="name", 24 | default="exports", 25 | help="configuration object name") 26 | parser.add_argument('--log-level', 27 | dest="log_level", 28 | default=DEFAULT_LOG_LEVEL_STRING, 29 | help="log level", 30 | choices=LOG_LEVEL_MAPPING.keys()) 31 | 32 | return parser.parse_args(args) 33 | 34 | 35 | def inner_main(args): 36 | log = logging.getLogger(__name__) 37 | options = get_args(args) 38 | update_log_level(options.log_level) 39 | file = options.config 40 | objname = options.name 41 | if file.endswith(".py"): 42 | config_base = try_file(file) 43 | if not config_base: 44 | # I don't think it works... but let's put it here 45 | config_base = try_file(file + ".py") 46 | else: 47 | config_base = try_file(file + ".py") 48 | if not config_base: 49 | filename = f"{file}, {file}.py" if file.endswith("py") else file 50 | log.error("Config file %s not found or not readable", filename) 51 | sys.exit(1) 52 | log.debug("Parsing config file: %s", file) 53 | if objname not in dir(config_base): 54 | log.error( 55 | "Exports not found. Please make sure your config in in a object called '%s'.", objname 56 | ) 57 | sys.exit(1) 58 | config_object = getattr(config_base, objname) 59 | log.debug("Config load:") 60 | displayable_config_object = sanitize(config_object) 61 | if not isinstance(config_object, object): 62 | log.error("Invalid config") 63 | sys.exit(1) 64 | log.debug( 65 | json.dumps(displayable_config_object, indent=2, sort_keys=True)) 66 | config_object = smart_rewrite(config_object) 67 | if is_libime_used(config_object) and shutil.which(LIBIME_BIN_NAME) is None: 68 | log.warning( 69 | "You are trying to generate fcitx dictionary, while %s doesn't seem to exist.", 70 | LIBIME_BIN_NAME 71 | ) 72 | log.warning( 73 | "This might cause issues. Please install libime: %s", LIBIME_REPOLOGY_URL 74 | ) 75 | build(config_object) 76 | 77 | 78 | def main(): 79 | setup_logger() 80 | inner_main(sys.argv[1:]) 81 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copied from https://github.com/ArtalkJS/Artalk/blob/master/.github/workflows/release.yml 2 | # MIT License 3 | # Copyright (c) 2018-present, qwqcode and other contributors 4 | 5 | name: Create release 6 | run-name: Release ${{ inputs.semver }} ${{ inputs.dry_run && '(dry-run)' || '' }} 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | on: 13 | workflow_dispatch: 14 | inputs: 15 | semver: 16 | type: choice 17 | description: Which version you want to increment? 18 | options: 19 | - patch 20 | - minor 21 | - major 22 | required: true 23 | custom_version: 24 | description: Manual Custom Version 25 | type: string 26 | required: false 27 | dry_run: 28 | description: "Dry run?" 29 | type: boolean 30 | default: false 31 | 32 | jobs: 33 | release: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout Code 37 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 38 | with: 39 | ref: master 40 | fetch-depth: 0 41 | 42 | - name: Setup Node 43 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 44 | with: 45 | node-version: 22.x 46 | registry-url: https://registry.npmjs.org/ 47 | 48 | - name: Setup semver 49 | run: npm install -g semver 50 | 51 | - name: Handle Version Number 52 | run: | 53 | PREV_VERSION="$(cat mw2fcitx/version.py | grep 'PKG_VERSION' | awk '{print $3}' | sed 's/^"//; s/"$//')" 54 | 55 | if [ -n "${{ inputs.custom_version }}" ]; then 56 | NEXT_VERSION="${{ inputs.custom_version }}" 57 | else 58 | NEXT_VERSION="$(semver --increment ${{ inputs.semver }} ${PREV_VERSION})" 59 | fi 60 | 61 | echo "PREV_VERSION=${PREV_VERSION}" >> $GITHUB_ENV 62 | echo "VERSION=${NEXT_VERSION}" >> $GITHUB_ENV 63 | 64 | - name: Print Next Version 65 | run: | 66 | echo "Version change: ${PREV_VERSION} -> ${VERSION}" 67 | 68 | - name: Modify version strings 69 | run: | 70 | # version.py 71 | sed -i 's/PKG_VERSION = "'$PREV_VERSION'"/PKG_VERSION = "'$VERSION'"/' mw2fcitx/version.py 72 | 73 | # pyproject.toml 74 | sed -i 's/version = "'$PREV_VERSION'"/version = "'$VERSION'"/' pyproject.toml 75 | 76 | - name: Print Git Diff 77 | run: git diff 78 | 79 | - name: Create Pull Request 80 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 81 | if: ${{ !inputs.dry_run }} 82 | with: 83 | branch: "rel/${{ env.VERSION }}" 84 | commit-message: "chore: rel ${{ env.VERSION }}" 85 | title: "Release ${{ env.VERSION }}" 86 | labels: release 87 | body: | 88 | ## Release ${{ env.VERSION }} 89 | 90 | This PR is auto-generated, please check the changelog and confirm the release. 😀 91 | 92 | The build workflow will be triggered after the PR is merged. 93 | committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 94 | author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 95 | assignees: ${{ github.actor }} 96 | -------------------------------------------------------------------------------- /mw2fcitx/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | from copy import deepcopy 4 | from importlib import import_module 5 | import os 6 | import logging 7 | from typing import List, Union 8 | from urllib3.util import Retry 9 | from requests import Session 10 | from requests.adapters import HTTPAdapter 11 | 12 | 13 | from .version import PKG_VERSION 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | def normalize(word): 19 | return word.strip() 20 | 21 | 22 | def sanitize(obj): 23 | res = deepcopy(obj) 24 | typ = type(res) 25 | if typ == type(sanitize): # function 26 | func_name = res.__name__ or "lambda" 27 | return f"[func {func_name}]" 28 | if typ == type({}): # object 29 | for i in res.keys(): 30 | res[i] = sanitize(res[i]) 31 | elif typ == type([]): # list 32 | fin = [] 33 | for i in res: 34 | fin.append(sanitize(i)) 35 | res = fin 36 | elif typ == type(1) or typ == type("1"): # number 37 | return str(res) 38 | else: # whatever 39 | return "[" + str(type(res)) + "]" 40 | return res 41 | 42 | 43 | def smart_rewrite(config_object): 44 | 45 | # If `generator` is not a list, make it a list 46 | generators = config_object["generator"] 47 | if not isinstance(generators, list): 48 | config_object["generator"] = [generators] 49 | 50 | # If `title_file_path` is not a list, make it a list 51 | title_file_path = config_object["source"].get("file_path") 52 | if isinstance(title_file_path, str): 53 | config_object["source"]['file_path'] = [title_file_path] 54 | 55 | return config_object 56 | 57 | 58 | def is_libime_used(config): 59 | generators = config.get('generator') or [] 60 | for i in generators: 61 | if i.get("use") == "pinyin": 62 | return True 63 | return False 64 | 65 | 66 | def dedup(arr: List[str]): 67 | return list(set(arr)) 68 | 69 | 70 | def create_requests_session(custom_user_agent: Union[str, None] = None): 71 | s = Session() 72 | retries = Retry( 73 | total=3, 74 | backoff_factor=1, 75 | ) 76 | s.headers.update({ 77 | "User-Agent": f"MW2Fcitx/{PKG_VERSION}; github.com/outloudvi/fcitx5-pinyin-moegirl", 78 | }) 79 | if custom_user_agent is not None: 80 | s.headers.update({ 81 | 'User-Agent': custom_user_agent 82 | }) 83 | s.mount('http://', HTTPAdapter(max_retries=retries)) 84 | s.mount('https://', HTTPAdapter(max_retries=retries)) 85 | return s 86 | 87 | 88 | def try_file(file): 89 | log.debug("Finding config file: %s", file) 90 | if not os.access(file, os.R_OK): 91 | log.error("File ({}) not readable.") 92 | return False 93 | file_realpath = os.path.realpath(file) 94 | log.debug("Config file path: %s", file_realpath) 95 | file_path = os.path.dirname(file_realpath) 96 | file_name = os.path.basename(file_realpath) 97 | module_name = re.sub(r"\.py$", "", file_name) 98 | config_file = False 99 | try: 100 | sys.path.insert(1, file_path) 101 | config_file = import_module(module_name) 102 | except Exception as e: 103 | log.error("Error reading config: %s", str(e)) 104 | return False 105 | finally: 106 | sys.path.remove(file_path) 107 | return config_file 108 | -------------------------------------------------------------------------------- /mw2fcitx/pipeline.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-outside-toplevel 2 | 3 | import json 4 | import logging 5 | import os 6 | import sys 7 | from typing import Callable, List, Union 8 | 9 | from .fetch import fetch_all_titles 10 | from .utils import dedup 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class MWFPipeline(): 16 | """ 17 | A pipeline for converting title lists to dictionaries. 18 | """ 19 | 20 | titles: list[str] 21 | words: list[str] 22 | 23 | def __init__(self, api_path=""): 24 | self.api_path = api_path 25 | self.titles = [] 26 | self.words = [] 27 | self.exports = "" 28 | self.dict = "" 29 | 30 | def load_titles(self, titles, limit=-1, replace=False): 31 | if isinstance(titles, str): 32 | titles = titles.split("\n") 33 | if limit >= 0: 34 | titles = titles[:limit] 35 | if replace: 36 | self.titles = titles 37 | else: 38 | self.titles.extend(titles) 39 | log.debug("%d title(s) imported.", len(titles)) 40 | self.words = self.titles 41 | 42 | def write_titles_to_file(self, filename): 43 | try: 44 | with open(filename, "w", encoding="utf-8") as file: 45 | file.write("\n".join(self.titles)) 46 | except Exception as e: 47 | log.error("File %s is not writable: %s", filename, str(e)) 48 | sys.exit(1) 49 | 50 | def post_load(self, **kwargs): 51 | if kwargs.get("output"): 52 | self.write_titles_to_file(kwargs.get("output")) 53 | 54 | def load_titles_from_file(self, filename, **kwargs): 55 | limit = kwargs.get("file_title_limit") or kwargs.get( 56 | "title_limit") or -1 57 | if not os.access(filename, os.R_OK): 58 | log.error( 59 | "File %s is not readable; " 60 | "remove this parameter (\"file_path\") or provide a readable file", filename 61 | ) 62 | sys.exit(1) 63 | with open(filename, "r", encoding="utf-8") as fp: 64 | self.load_titles(fp.read(), limit=limit) 65 | 66 | def fetch_titles(self, **kwargs): 67 | titles = fetch_all_titles(self.api_path, **kwargs) 68 | self.load_titles(titles) 69 | self.post_load(**kwargs) 70 | 71 | def reset_words(self): 72 | self.words = self.titles 73 | 74 | def convert_to_words(self, pipelines: Union[ 75 | Callable[[List[str]], List[str]], 76 | List[Callable[[List[str]], List[str]]]]): 77 | if callable(pipelines): 78 | pipelines = [pipelines] 79 | log.debug("Running %d pipelines", len(pipelines)) 80 | cnt = 0 81 | titles = self.titles 82 | for i in pipelines: 83 | cnt += 1 84 | log.debug( 85 | "Running pipeline %d/%d (%s)", 86 | cnt, 87 | len(pipelines), 88 | i.__name__ or 'anonymous function' 89 | ) 90 | titles = i(titles) 91 | log.debug("Deduplicating %d items", len(titles)) 92 | self.words = dedup(titles) 93 | log.debug( 94 | "Deduplication completed. %d items left.", len(self.words)) 95 | 96 | def export_words(self, converter="pypinyin", **kwargs): 97 | # "opencc" is an alias for backward compatibility 98 | if converter in ("pypinyin", "opencc"): 99 | log.debug("Exporting %d words with OpenCC", len(self.words)) 100 | from mw2fcitx.exporters.pypinyin import export 101 | fixfile_path = kwargs.get('fixfile') 102 | if fixfile_path is not None: 103 | with open(fixfile_path, "r", encoding="utf-8") as fp: 104 | kwargs["fix_table"] = json.load(fp) 105 | self.exports = export(self.words, **kwargs) 106 | elif callable(converter): 107 | log.debug( 108 | "Exporting %d words with custom converter", len(self.words)) 109 | self.exports = converter(self.words, **kwargs) 110 | else: 111 | log.error("No such exporter: %s", converter) 112 | 113 | def generate_dict(self, generator="pinyin", **kwargs): 114 | if generator == "pinyin": 115 | from mw2fcitx.dictgen import pinyin 116 | dest = kwargs.get("output") 117 | if not dest: 118 | log.error( 119 | "Dictgen 'pinyin' can only output to files.\n" + 120 | "Please give the file path in the 'output' argument.") 121 | return 122 | pinyin(self.exports, **kwargs) 123 | elif generator == "rime": 124 | from mw2fcitx.dictgen import rime 125 | self.dict = rime(self.exports, **kwargs) 126 | else: 127 | log.error("No such dictgen: %s", generator) 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > 如果您需要下载**萌娘百科 (zh.moegirl.org.cn) 词库**,请[参见此页](https://github.com/outloudvi/mw2fcitx/wiki/fcitx5-pinyin-moegirl)。 3 | > 4 | > For the **pre-built dictionary for Moegirlpedia** (zh.moegirl.org.cn), see [the wiki](https://github.com/outloudvi/mw2fcitx/wiki/fcitx5-pinyin-moegirl#extra-dictionaries). 5 | 6 | > [!WARNING] 7 | > `mw2fcitx` 0.20.0 包含一些主要和繁简转换相关的 breaking changes。请查看 [BREAKING_CHANGES.md](./BREAKING_CHANGES.md) 了解更多信息。 8 | 9 | --- 10 | 11 | # mw2fcitx 12 | 13 | Build fcitx5/RIME dictionaries from MediaWiki sites. 14 | 15 | [![PyPI](https://img.shields.io/pypi/v/mw2fcitx)](https://pypi.org/project/mw2fcitx/) 16 | [![Tests](https://github.com/outloudvi/mw2fcitx/actions/workflows/test.yml/badge.svg)](https://github.com/outloudvi/mw2fcitx/actions/workflows/test.yml) 17 | [![codecov: Coverage](https://codecov.io/gh/outloudvi/mw2fcitx/graph/badge.svg?token=1RP1099913)](https://codecov.io/gh/outloudvi/mw2fcitx) 18 | 19 | ```sh 20 | pip install mw2fcitx 21 | # or if you want to just install for current user 22 | pip install mw2fcitx --user 23 | # or if you want to just run it (needs Pipx) 24 | pipx run mw2fcitx 25 | # or if you need to use OpenCC for text conversion 26 | pip install mw2fcitx[opencc] 27 | ``` 28 | 29 | ## CLI Usage 30 | 31 | ``` 32 | mw2fcitx -c config_script.py 33 | ``` 34 | 35 | ## Configuration Script Format 36 | 37 | ```python 38 | from mw2fcitx.tweaks.moegirl import tweaks 39 | # By default we assume the configuration is located at a variable 40 | # called "exports". 41 | # You can change this with `-n any_name` in the CLI. 42 | 43 | exports = { 44 | # Source configurations. 45 | "source": { 46 | # MediaWiki api.php path, if to fetch titles from online. 47 | "api_path": "https://zh.moegirl.org.cn/api.php", 48 | # Title file path, if to fetch titles from local file. (optional) 49 | # Can be a path or a list of paths. 50 | "file_path": ["titles.txt"], 51 | "kwargs": { 52 | # Title number limit for fetching. (optional) 53 | "title_limit": 120, 54 | # Title number limit for fetching via API. (optional) 55 | # Overrides title_limit. 56 | "api_title_limit": 120, 57 | # Title number limit for each fetch via file. (optional) 58 | # Overrides title_limit. 59 | "file_title_limit": 60, 60 | # Partial session file on exception (optional) 61 | "partial": "partial.json", 62 | # Title list export path. (optional) 63 | "output": "titles.txt", 64 | # Delay between MediaWiki API requests in seconds. (optional) 65 | "request_delay": 2, 66 | # Deprecated. Please use `source.kwargs.api_params.aplimit` instead. (optional) 67 | "aplimit": "max", 68 | # Override ALL parameters while calling MediaWiki API. 69 | "api_params": { 70 | # Results per API request; same as `aplimit` in MediaWiki docs. (optional) 71 | "aplimit": "max" 72 | }, 73 | # User-Agent used while requesting the API. (optional) 74 | "user_agent": "MW2Fcitx/development" 75 | } 76 | }, 77 | # Tweaks configurations as an list. 78 | # Every tweak function accepts a list of titles and return 79 | # a list of title. 80 | "tweaks": 81 | tweaks, 82 | # Converter configurations. 83 | "converter": { 84 | # pypinyin is a built-in converter. 85 | # For custom converter functions, just give the function itself. 86 | "use": "pypinyin", 87 | "kwargs": { 88 | # Replace "m" to "mu" and "n" to "en". Default: False. 89 | # See more in https://github.com/outloudvi/mw2fcitx/issues/29 . 90 | "disable_instinct_pinyin": False, 91 | # Pinyin results to replace. (optional) 92 | # Format: { "汉字": "pin'yin" } 93 | # The result will be sent into `pypinyin` as a phrase, so words containing this phrase are also affected. 94 | "fixfile": "fixfile.json", 95 | # Characters to omit during pinyin conversion. (optional) 96 | # These characters will be automatically removed while trying to convert to pinyin. 97 | # As a result, words containing these characters will not be skipped in the dictionary. 98 | "characters_to_omit": ["·"], 99 | } 100 | }, 101 | # Generator configurations. 102 | "generator": [{ 103 | # rime is a built-in generator. 104 | # For custom generator functions, just give the function itself. 105 | "use": "rime", 106 | "kwargs": { 107 | # Destination dictionary filename. (optional) 108 | "output": "moegirl.dict.yml" 109 | } 110 | }, { 111 | # pinyin is a built-in generator. 112 | # This generator depends on `libime`. 113 | "use": "pinyin", 114 | "kwargs": { 115 | # Destination dictionary filename. (mandatory) 116 | "output": "moegirl.dict" 117 | } 118 | }] 119 | } 120 | ``` 121 | 122 | A sample config file is here: [`sample_config.py`](https://github.com/outloudvi/mw2fcitx/blob/master/mw2fcitx/sample_config.py) 123 | 124 | ## Advanced mode 125 | 126 | As `mw2fcitx` provides the feature to append and override MediaWiki API parameters, it is possible to use it to collect other types of lists in addition to [`allpages`](https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allpages). Please note that if `list`, `action` or `format` is overriden in `api_params`, `mw2fcitx` will not automatically append any default parameter (except for `format`) while sending MediaWiki API requests. Please determine the parameters needed by yourself. [A configuration in tests](tests/cli/confs/conf_api_list_categorymembers.py) may be helpful for your reference. 127 | 128 | ## Breaking changes across versions 129 | 130 | Read [BREAKING_CHANGES.md](./BREAKING_CHANGES.md) for details. 131 | 132 | ## License 133 | 134 | [MIT License](https://github.com/outloudvi/mw2fcitx/blob/master/LICENSE) 135 | -------------------------------------------------------------------------------- /mw2fcitx/fetch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import json 4 | from os import access, R_OK 5 | import time 6 | from typing import Any, List, Union 7 | 8 | from requests import Session 9 | 10 | from .const import ADVANCED_MODE_TRIGGER_PARAMETER_NAMES, \ 11 | PARTIAL_CONTINUE_DICT, \ 12 | PARTIAL_DEPRECATED_APCONTINUE, \ 13 | PARTIAL_TITLES 14 | from .utils import create_requests_session 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | def save_to_partial(partial_path: str, titles: List[str], continue_dict: dict): 20 | ret = {PARTIAL_CONTINUE_DICT: continue_dict, PARTIAL_TITLES: titles} 21 | try: 22 | with open(partial_path, "w", encoding="utf-8") as fp: 23 | fp.write(json.dumps(ret, ensure_ascii=False)) 24 | log.debug("Partial session saved to %s", partial_path) 25 | except Exception as e: 26 | log.error(str(e)) 27 | 28 | 29 | def resume_from_partial(partial_path: str) -> tuple[List[str], dict]: 30 | if not access(partial_path, R_OK): 31 | log.warning("Cannot read partial session: %s", partial_path) 32 | return ([], {}) 33 | try: 34 | with open(partial_path, "r", encoding="utf-8") as fp: 35 | partial_data = json.load(fp) 36 | titles = partial_data.get(PARTIAL_TITLES, []) 37 | deprecated_apcontinue = partial_data.get( 38 | PARTIAL_DEPRECATED_APCONTINUE, None) 39 | continue_dict = partial_data.get(PARTIAL_CONTINUE_DICT, None) 40 | if continue_dict is None and deprecated_apcontinue is not None: 41 | continue_dict = { 42 | "apcontinue": deprecated_apcontinue 43 | } 44 | return (titles, continue_dict) 45 | except Exception as e: 46 | log.error(str(e)) 47 | log.error("Failed to parse partial session") 48 | return ([], {}) 49 | 50 | 51 | def warn_advanced_mode(custom_api_params: dict, triggerer_param_names: List[str]) -> bool: 52 | is_advanced_mode = False 53 | for param_name in triggerer_param_names: 54 | if param_name in custom_api_params: 55 | log.warning( 56 | "I'm seeing `%s` in `api_params`. " 57 | "Advanced Mode is enabled. " 58 | "No parameter except for `format` will be injected automatically.", param_name 59 | ) 60 | is_advanced_mode = True 61 | 62 | return is_advanced_mode 63 | 64 | 65 | def populate_api_params(custom_api_params: dict, deprecated_aplimit: Union[None, int]) -> dict: 66 | api_params = { 67 | "format": "json" 68 | } 69 | 70 | if not warn_advanced_mode(custom_api_params, ADVANCED_MODE_TRIGGER_PARAMETER_NAMES): 71 | # Deprecated `aplimit` 72 | if deprecated_aplimit is not None: 73 | log.warning( 74 | "Warn: `source.kwargs.aplimit` is deprecated - " 75 | "please use `source.kwargs.api_param.aplimit` instead.") 76 | aplimit = int(deprecated_aplimit) \ 77 | if deprecated_aplimit != "max" and deprecated_aplimit is not None \ 78 | else "max" 79 | if "aplimit" not in custom_api_params: 80 | custom_api_params["aplimit"] = aplimit 81 | 82 | api_params.update({ 83 | # default params 84 | "aplimit": "max", 85 | "action": "query", 86 | "list": "allpages", 87 | }) 88 | 89 | api_params.update(custom_api_params) 90 | return api_params 91 | 92 | 93 | def fetch_all_titles(api_url: str, **kwargs) -> List[str]: 94 | title_limit = kwargs.get( 95 | "api_title_limit") or kwargs.get("title_limit") or -1 96 | log_msg = f"Fetching titles from {api_url}" + \ 97 | (f" with a limit of {title_limit}" if title_limit != -1 else "") 98 | log.debug(log_msg) 99 | titles = [] 100 | partial_path = kwargs.get("partial") 101 | time_wait = float(kwargs.get("request_delay", "2")) 102 | custom_api_params = kwargs.get("api_params", {}) 103 | if not isinstance(custom_api_params, dict): 104 | log.error( 105 | "Type of `api_params` is not dict or None, but %s", type(custom_api_params)) 106 | sys.exit(1) 107 | 108 | api_params = populate_api_params(custom_api_params, kwargs.get("aplimit")) 109 | if partial_path is not None: 110 | log.info("Partial session will be saved/read: %s", partial_path) 111 | [titles, continue_dict] = resume_from_partial(partial_path) 112 | if continue_dict is not None: 113 | api_params.update(continue_dict) 114 | log.info( 115 | "%d titles found. Continuing from %s", len(titles), continue_dict) 116 | s = create_requests_session(kwargs.get("user_agent")) 117 | resp = s.get(api_url, params=api_params) 118 | initial_data = resp.json() 119 | titles = fetch_all_titles_inner( 120 | titles, 121 | initial_data, 122 | title_limit, 123 | api_url, 124 | api_params, 125 | partial_path, 126 | time_wait, 127 | s 128 | ) 129 | log.info("Finished.") 130 | return titles 131 | 132 | 133 | def fetch_all_titles_inner( 134 | # pylint: disable=too-many-arguments,too-many-positional-arguments 135 | titles: List[str], 136 | initial_data: Any, 137 | title_limit: int, 138 | api_url: str, 139 | initial_api_params: dict, 140 | partial_path: Union[str, None], 141 | time_wait: float, 142 | s: Session 143 | ) -> List[str]: 144 | data = initial_data 145 | 146 | while True: 147 | if "error" in data: 148 | error_code = data["error"].get("code", "?") 149 | error_msg = data["error"].get("info", str(data["error"])) 150 | log.error( 151 | "MediaWiki API error: [code: %s] %s", error_code, error_msg 152 | ) 153 | if not "query" in data: 154 | log.error( 155 | "No `query` found in response. " 156 | "Please check the response body for any potential issues:" 157 | ) 158 | log.error(data) 159 | sys.exit(1) 160 | 161 | for (_, item_value) in data["query"].items(): 162 | titles += list(map(lambda x: x["title"], item_value)) 163 | if title_limit != -1 and len(titles) >= title_limit: 164 | titles = titles[:title_limit] 165 | break 166 | log.debug("Got %s pages", len(titles)) 167 | if "continue" in data: 168 | time.sleep(time_wait) 169 | continue_dict = {} 170 | try: 171 | continue_dict = data["continue"] 172 | log.debug("Continuing from %s", continue_dict) 173 | api_params = initial_api_params.copy() 174 | api_params.update(continue_dict) 175 | data = s.get(api_url, params=api_params).json() 176 | except Exception as e: 177 | if isinstance(e, KeyboardInterrupt): 178 | log.error("Keyboard interrupt received. Stopping.") 179 | else: 180 | log.error(str(e)) 181 | if partial_path: 182 | save_to_partial(partial_path, titles, continue_dict) 183 | sys.exit(1) 184 | else: 185 | break 186 | 187 | return titles 188 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [{name = "Outvi V", email = "oss+pypi@outv.im>"}] 3 | dependencies = [ 4 | "pypinyin (>=0.54.0,<0.55.0)", 5 | "pyyaml (>=6.0.2,<7.0.0)", 6 | "requests (>=2.32.4,<3.0.0)", 7 | ] 8 | description = "Build fcitx5/RIME dictionaries from MediaWiki sites" 9 | dynamic = ["classifiers"] 10 | keywords = ["fcitx", "dictionary"] 11 | license = "MIT" 12 | name = "mw2fcitx" 13 | readme = "README.md" 14 | requires-python = ">=3.9" 15 | version = "0.24.1" 16 | 17 | [project.scripts] 18 | mw2fcitx = 'mw2fcitx.main:main' 19 | 20 | [project.urls] 21 | Discussions = "https://github.com/outloudvi/mw2fcitx/discussions" 22 | "Issue Tracker" = "https://github.com/outloudvi/mw2fcitx/issues" 23 | repository = "https://github.com/outloudvi/mw2fcitx" 24 | 25 | [tool.poetry] 26 | classifiers = [ 27 | 'Topic :: Software Development :: Libraries :: Python Modules', 28 | 'Development Status :: 4 - Beta', 29 | 'Environment :: Console', 30 | 'Topic :: Text Processing', 31 | 'Topic :: Utilities', 32 | ] 33 | include = [ 34 | 'mw2fcitx/**/*.json', 35 | ] 36 | package-mode = true 37 | packages = [ 38 | {include = "mw2fcitx"}, 39 | {include = "tests", format = "sdist"}, 40 | ] 41 | 42 | [project.optional-dependencies] 43 | opencc = [ 44 | "OpenCC (>=1.1.9,<2.0.0)", 45 | ] 46 | 47 | [tool.poetry.dependencies] 48 | python = ">=3.9,<4.0" 49 | 50 | [tool.poetry.group.dev.dependencies] 51 | autopep8 = "^2.3.1" 52 | coverage = "^7.6.0" 53 | pylint = "^3.2.5" 54 | pytest = "^8.3.1" 55 | pytest-cov = "^5.0.0" 56 | 57 | [build-system] 58 | build-backend = "poetry.core.masonry.api" 59 | requires = ["poetry-core"] 60 | 61 | [tool.yapf] 62 | based_on_style = "google" 63 | 64 | [tool.coverage.run] 65 | branch = true 66 | omit = [ 67 | "*/.venv/*", 68 | "mw2fcitx/sample_config.py", 69 | "tests/cli/conf_*.py", 70 | ] 71 | 72 | [tool.coverage.html] 73 | directory = "coverage_html_report" 74 | 75 | [tool.pylint.main] 76 | # Analyse import fallback blocks. This can be used to support both Python 2 and 3 77 | # compatible code, which means that the block might have code that exists only in 78 | # one or another interpreter, leading to false positives when analysed. 79 | # analyse-fallback-blocks = 80 | 81 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint in 82 | # a server-like mode. 83 | # clear-cache-post-run = 84 | 85 | # Always return a 0 (non-error) status code, even if lint errors are found. This 86 | # is primarily useful in continuous integration scripts. 87 | # exit-zero = 88 | 89 | # A comma-separated list of package or module names from where C extensions may 90 | # be loaded. Extensions are loading into the active Python interpreter and may 91 | # run arbitrary code. 92 | # extension-pkg-allow-list = 93 | 94 | # A comma-separated list of package or module names from where C extensions may 95 | # be loaded. Extensions are loading into the active Python interpreter and may 96 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 97 | # for backward compatibility.) 98 | # extension-pkg-whitelist = 99 | 100 | # Return non-zero exit code if any of these messages/categories are detected, 101 | # even if score is above --fail-under value. Syntax same as enable. Messages 102 | # specified are enabled, while categories only check already-enabled messages. 103 | # fail-on = 104 | 105 | # Specify a score threshold under which the program will exit with error. 106 | fail-under = 10 107 | 108 | # Interpret the stdin as a python script, whose filename needs to be passed as 109 | # the module_or_package argument. 110 | # from-stdin = 111 | 112 | # Files or directories to be skipped. They should be base names, not paths. 113 | ignore = ["CVS"] 114 | 115 | # Add files or directories matching the regular expressions patterns to the 116 | # ignore-list. The regex matches against paths and can be in Posix or Windows 117 | # format. Because '\\' represents the directory delimiter on Windows systems, it 118 | # can't be used as an escape character. 119 | # ignore-paths = 120 | 121 | # Files or directories matching the regular expression patterns are skipped. The 122 | # regex matches against base names, not paths. The default value ignores Emacs 123 | # file locks 124 | ignore-patterns = ["^\\.#"] 125 | 126 | # List of module names for which member attributes should not be checked and will 127 | # not be imported (useful for modules/projects where namespaces are manipulated 128 | # during runtime and thus existing member attributes cannot be deduced by static 129 | # analysis). It supports qualified module names, as well as Unix pattern 130 | # matching. 131 | # ignored-modules = 132 | 133 | # Python code to execute, usually for sys.path manipulation such as 134 | # pygtk.require(). 135 | # init-hook = 136 | 137 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 138 | # number of processors available to use, and will cap the count on Windows to 139 | # avoid hangs. 140 | jobs = 1 141 | 142 | # Control the amount of potential inferred values when inferring a single object. 143 | # This can help the performance when dealing with large functions or complex, 144 | # nested conditions. 145 | limit-inference-results = 100 146 | 147 | # List of plugins (as comma separated values of python module names) to load, 148 | # usually to register additional checkers. 149 | # load-plugins = 150 | 151 | # Pickle collected data for later comparisons. 152 | persistent = true 153 | 154 | # Resolve imports to .pyi stubs if available. May reduce no-member messages and 155 | # increase not-an-iterable messages. 156 | # prefer-stubs = 157 | 158 | # Minimum Python version to use for version dependent checks. Will default to the 159 | # version used to run pylint. 160 | py-version = "3.9" 161 | 162 | # Discover python modules and packages in the file system subtree. 163 | # recursive = 164 | 165 | # Add paths to the list of the source roots. Supports globbing patterns. The 166 | # source root is an absolute path or a path relative to the current working 167 | # directory used to determine a package namespace for modules located under the 168 | # source root. 169 | # source-roots = 170 | 171 | # When enabled, pylint would attempt to guess common misconfiguration and emit 172 | # user-friendly hints instead of false-positive error messages. 173 | suggestion-mode = true 174 | 175 | # Allow loading of arbitrary C extensions. Extensions are imported into the 176 | # active Python interpreter and may run arbitrary code. 177 | # unsafe-load-any-extension = 178 | 179 | [tool.pylint.basic] 180 | # Naming style matching correct argument names. 181 | argument-naming-style = "snake_case" 182 | 183 | # Regular expression matching correct argument names. Overrides argument-naming- 184 | # style. If left empty, argument names will be checked with the set naming style. 185 | # argument-rgx = 186 | 187 | # Naming style matching correct attribute names. 188 | attr-naming-style = "snake_case" 189 | 190 | # Regular expression matching correct attribute names. Overrides attr-naming- 191 | # style. If left empty, attribute names will be checked with the set naming 192 | # style. 193 | # attr-rgx = 194 | 195 | # Bad variable names which should always be refused, separated by a comma. 196 | bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] 197 | 198 | # Bad variable names regexes, separated by a comma. If names match any regex, 199 | # they will always be refused 200 | # bad-names-rgxs = 201 | 202 | # Naming style matching correct class attribute names. 203 | class-attribute-naming-style = "any" 204 | 205 | # Regular expression matching correct class attribute names. Overrides class- 206 | # attribute-naming-style. If left empty, class attribute names will be checked 207 | # with the set naming style. 208 | # class-attribute-rgx = 209 | 210 | # Naming style matching correct class constant names. 211 | class-const-naming-style = "UPPER_CASE" 212 | 213 | # Regular expression matching correct class constant names. Overrides class- 214 | # const-naming-style. If left empty, class constant names will be checked with 215 | # the set naming style. 216 | # class-const-rgx = 217 | 218 | # Naming style matching correct class names. 219 | class-naming-style = "PascalCase" 220 | 221 | # Regular expression matching correct class names. Overrides class-naming-style. 222 | # If left empty, class names will be checked with the set naming style. 223 | # class-rgx = 224 | 225 | # Naming style matching correct constant names. 226 | const-naming-style = "UPPER_CASE" 227 | 228 | # Regular expression matching correct constant names. Overrides const-naming- 229 | # style. If left empty, constant names will be checked with the set naming style. 230 | # const-rgx = 231 | 232 | # Minimum line length for functions/classes that require docstrings, shorter ones 233 | # are exempt. 234 | docstring-min-length = -1 235 | 236 | # Naming style matching correct function names. 237 | function-naming-style = "snake_case" 238 | 239 | # Regular expression matching correct function names. Overrides function-naming- 240 | # style. If left empty, function names will be checked with the set naming style. 241 | # function-rgx = 242 | 243 | # Good variable names which should always be accepted, separated by a comma. 244 | good-names = ["i", "j", "k", "ex", "Run", "_"] 245 | 246 | # Good variable names regexes, separated by a comma. If names match any regex, 247 | # they will always be accepted 248 | # good-names-rgxs = 249 | 250 | # Include a hint for the correct naming format with invalid-name. 251 | # include-naming-hint = 252 | 253 | # Naming style matching correct inline iteration names. 254 | inlinevar-naming-style = "any" 255 | 256 | # Regular expression matching correct inline iteration names. Overrides 257 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 258 | # with the set naming style. 259 | # inlinevar-rgx = 260 | 261 | # Naming style matching correct method names. 262 | method-naming-style = "snake_case" 263 | 264 | # Regular expression matching correct method names. Overrides method-naming- 265 | # style. If left empty, method names will be checked with the set naming style. 266 | # method-rgx = 267 | 268 | # Naming style matching correct module names. 269 | module-naming-style = "snake_case" 270 | 271 | # Regular expression matching correct module names. Overrides module-naming- 272 | # style. If left empty, module names will be checked with the set naming style. 273 | # module-rgx = 274 | 275 | # Colon-delimited sets of names that determine each other's naming style when the 276 | # name regexes allow several styles. 277 | # name-group = 278 | 279 | # Regular expression which should only match function or class names that do not 280 | # require a docstring. 281 | no-docstring-rgx = "^_" 282 | 283 | # List of decorators that produce properties, such as abc.abstractproperty. Add 284 | # to this list to register other decorators that produce valid properties. These 285 | # decorators are taken in consideration only for invalid-name. 286 | property-classes = ["abc.abstractproperty"] 287 | 288 | # Regular expression matching correct type alias names. If left empty, type alias 289 | # names will be checked with the set naming style. 290 | # typealias-rgx = 291 | 292 | # Regular expression matching correct type variable names. If left empty, type 293 | # variable names will be checked with the set naming style. 294 | # typevar-rgx = 295 | 296 | # Naming style matching correct variable names. 297 | variable-naming-style = "snake_case" 298 | 299 | # Regular expression matching correct variable names. Overrides variable-naming- 300 | # style. If left empty, variable names will be checked with the set naming style. 301 | # variable-rgx = 302 | 303 | [tool.pylint.classes] 304 | # Warn about protected attribute access inside special methods 305 | # check-protected-access-in-special-methods = 306 | 307 | # List of method names used to declare (i.e. assign) instance attributes. 308 | defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"] 309 | 310 | # List of member names, which should be excluded from the protected access 311 | # warning. 312 | exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"] 313 | 314 | # List of valid names for the first argument in a class method. 315 | valid-classmethod-first-arg = ["cls"] 316 | 317 | # List of valid names for the first argument in a metaclass class method. 318 | valid-metaclass-classmethod-first-arg = ["mcs"] 319 | 320 | [tool.pylint.design] 321 | # List of regular expressions of class ancestor names to ignore when counting 322 | # public methods (see R0903) 323 | # exclude-too-few-public-methods = 324 | 325 | # List of qualified class names to ignore when counting class parents (see R0901) 326 | # ignored-parents = 327 | 328 | # Maximum number of arguments for function / method. 329 | max-args = 5 330 | 331 | # Maximum number of attributes for a class (see R0902). 332 | max-attributes = 7 333 | 334 | # Maximum number of boolean expressions in an if statement (see R0916). 335 | max-bool-expr = 5 336 | 337 | # Maximum number of branch for function / method body. 338 | max-branches = 12 339 | 340 | # Maximum number of locals for function / method body. 341 | max-locals = 15 342 | 343 | # Maximum number of parents for a class (see R0901). 344 | max-parents = 7 345 | 346 | # Maximum number of public methods for a class (see R0904). 347 | max-public-methods = 20 348 | 349 | # Maximum number of return / yield for function / method body. 350 | max-returns = 6 351 | 352 | # Maximum number of statements in function / method body. 353 | max-statements = 50 354 | 355 | # Minimum number of public methods for a class (see R0903). 356 | min-public-methods = 2 357 | 358 | [tool.pylint.exceptions] 359 | # Exceptions that will emit a warning when caught. 360 | overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] 361 | 362 | [tool.pylint.format] 363 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 364 | # expected-line-ending-format = 365 | 366 | # Regexp for a line that is allowed to be longer than the limit. 367 | ignore-long-lines = "^\\s*(# )??$" 368 | 369 | # Number of spaces of indent required inside a hanging or continued line. 370 | indent-after-paren = 4 371 | 372 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 373 | # tab). 374 | indent-string = " " 375 | 376 | # Maximum number of characters on a single line. 377 | max-line-length = 100 378 | 379 | # Maximum number of lines in a module. 380 | max-module-lines = 1000 381 | 382 | # Allow the body of a class to be on the same line as the declaration if body 383 | # contains single statement. 384 | # single-line-class-stmt = 385 | 386 | # Allow the body of an if to be on the same line as the test if there is no else. 387 | # single-line-if-stmt = 388 | 389 | [tool.pylint.imports] 390 | # List of modules that can be imported at any level, not just the top level one. 391 | # allow-any-import-level = 392 | 393 | # Allow explicit reexports by alias from a package __init__. 394 | # allow-reexport-from-package = 395 | 396 | # Allow wildcard imports from modules that define __all__. 397 | # allow-wildcard-with-all = 398 | 399 | # Deprecated modules which should not be used, separated by a comma. 400 | # deprecated-modules = 401 | 402 | # Output a graph (.gv or any supported image format) of external dependencies to 403 | # the given file (report RP0402 must not be disabled). 404 | # ext-import-graph = 405 | 406 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 407 | # external) dependencies to the given file (report RP0402 must not be disabled). 408 | # import-graph = 409 | 410 | # Output a graph (.gv or any supported image format) of internal dependencies to 411 | # the given file (report RP0402 must not be disabled). 412 | # int-import-graph = 413 | 414 | # Force import order to recognize a module as part of the standard compatibility 415 | # libraries. 416 | # known-standard-library = 417 | 418 | # Force import order to recognize a module as part of a third party library. 419 | known-third-party = ["enchant"] 420 | 421 | # Couples of modules and preferred modules, separated by a comma. 422 | # preferred-modules = 423 | 424 | [tool.pylint.logging] 425 | # The type of string formatting that logging methods do. `old` means using % 426 | # formatting, `new` is for `{}` formatting. 427 | logging-format-style = "old" 428 | 429 | # Logging modules to check that the string format arguments are in logging 430 | # function parameter format. 431 | logging-modules = ["logging"] 432 | 433 | [tool.pylint."messages control"] 434 | # Only show warnings with the listed confidence levels. Leave empty to show all. 435 | # Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 436 | confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] 437 | 438 | # Disable the message, report, category or checker with the given id(s). You can 439 | # either give multiple identifiers separated by comma (,) or put this option 440 | # multiple times (only on the command line, not in the configuration file where 441 | # it should appear only once). You can also use "--disable=all" to disable 442 | # everything first and then re-enable specific checks. For example, if you want 443 | # to run only the similarities checker, you can use "--disable=all 444 | # --enable=similarities". If you want to run only the classes checker, but have 445 | # no Warning level messages displayed, use "--disable=all --enable=classes 446 | # --disable=W". 447 | disable = [ 448 | "raw-checker-failed", 449 | "bad-inline-option", 450 | "locally-disabled", 451 | "file-ignored", 452 | "suppressed-message", 453 | "useless-suppression", 454 | "deprecated-pragma", 455 | "use-symbolic-message-instead", 456 | "use-implicit-booleaness-not-comparison-to-string", 457 | "use-implicit-booleaness-not-comparison-to-zero", 458 | "missing-function-docstring", 459 | "missing-module-docstring", 460 | "broad-exception-caught", 461 | ] 462 | 463 | # Enable the message, report, category or checker with the given id(s). You can 464 | # either give multiple identifier separated by comma (,) or put this option 465 | # multiple time (only on the command line, not in the configuration file where it 466 | # should appear only once). See also the "--disable" option for examples. 467 | # enable = 468 | 469 | [tool.pylint.method_args] 470 | # List of qualified names (i.e., library.method) which require a timeout 471 | # parameter e.g. 'requests.api.get,requests.api.post' 472 | timeout-methods = [ 473 | "requests.api.delete", 474 | "requests.api.get", 475 | "requests.api.head", 476 | "requests.api.options", 477 | "requests.api.patch", 478 | "requests.api.post", 479 | "requests.api.put", 480 | "requests.api.request", 481 | ] 482 | 483 | [tool.pylint.miscellaneous] 484 | # List of note tags to take in consideration, separated by a comma. 485 | notes = ["FIXME", "XXX", "TODO"] 486 | 487 | # Regular expression of note tags to take in consideration. 488 | # notes-rgx = 489 | 490 | [tool.pylint.refactoring] 491 | # Maximum number of nested blocks for function / method body 492 | max-nested-blocks = 5 493 | 494 | # Complete name of functions that never returns. When checking for inconsistent- 495 | # return-statements if a never returning function is called then it will be 496 | # considered as an explicit return statement and no message will be printed. 497 | never-returning-functions = ["sys.exit", "argparse.parse_error"] 498 | 499 | # Let 'consider-using-join' be raised when the separator to join on would be non- 500 | # empty (resulting in expected fixes of the type: ``"- " + " - ".join(items)``) 501 | suggest-join-with-non-empty-separator = true 502 | 503 | [tool.pylint.reports] 504 | # Python expression which should return a score less than or equal to 10. You 505 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 506 | # 'convention', and 'info' which contain the number of messages in each category, 507 | # as well as 'statement' which is the total number of statements analyzed. This 508 | # score is used by the global evaluation report (RP0004). 509 | evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" 510 | 511 | # Template used to display messages. This is a python new-style format string 512 | # used to format the message information. See doc for all details. 513 | # msg-template = 514 | 515 | # Set the output format. Available formats are: text, parseable, colorized, json2 516 | # (improved json format), json (old json format) and msvs (visual studio). You 517 | # can also give a reporter class, e.g. mypackage.mymodule.MyReporterClass. 518 | # output-format = 519 | 520 | # Tells whether to display a full report or only the messages. 521 | # reports = 522 | 523 | # Activate the evaluation score. 524 | score = true 525 | 526 | [tool.pylint.similarities] 527 | # Comments are removed from the similarity computation 528 | ignore-comments = true 529 | 530 | # Docstrings are removed from the similarity computation 531 | ignore-docstrings = true 532 | 533 | # Imports are removed from the similarity computation 534 | ignore-imports = true 535 | 536 | # Signatures are removed from the similarity computation 537 | ignore-signatures = true 538 | 539 | # Minimum lines number of a similarity. 540 | min-similarity-lines = 4 541 | 542 | [tool.pylint.spelling] 543 | # Limits count of emitted suggestions for spelling mistakes. 544 | max-spelling-suggestions = 4 545 | 546 | # Spelling dictionary name. No available dictionaries : You need to install both 547 | # the python package and the system dependency for enchant to work. 548 | # spelling-dict = 549 | 550 | # List of comma separated words that should be considered directives if they 551 | # appear at the beginning of a comment and should not be checked. 552 | spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" 553 | 554 | # List of comma separated words that should not be checked. 555 | # spelling-ignore-words = 556 | 557 | # A path to a file that contains the private dictionary; one word per line. 558 | # spelling-private-dict-file = 559 | 560 | # Tells whether to store unknown words to the private dictionary (see the 561 | # --spelling-private-dict-file option) instead of raising a message. 562 | # spelling-store-unknown-words = 563 | 564 | [tool.pylint.typecheck] 565 | # List of decorators that produce context managers, such as 566 | # contextlib.contextmanager. Add to this list to register other decorators that 567 | # produce valid context managers. 568 | contextmanager-decorators = ["contextlib.contextmanager"] 569 | 570 | # List of members which are set dynamically and missed by pylint inference 571 | # system, and so shouldn't trigger E1101 when accessed. Python regular 572 | # expressions are accepted. 573 | # generated-members = 574 | 575 | # Tells whether missing members accessed in mixin class should be ignored. A 576 | # class is considered mixin if its name matches the mixin-class-rgx option. 577 | # Tells whether to warn about missing members when the owner of the attribute is 578 | # inferred to be None. 579 | ignore-none = true 580 | 581 | # This flag controls whether pylint should warn about no-member and similar 582 | # checks whenever an opaque object is returned when inferring. The inference can 583 | # return multiple potential results while evaluating a Python object, but some 584 | # branches might not be evaluated, which results in partial inference. In that 585 | # case, it might be useful to still emit no-member and other checks for the rest 586 | # of the inferred objects. 587 | ignore-on-opaque-inference = true 588 | 589 | # List of symbolic message names to ignore for Mixin members. 590 | ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] 591 | 592 | # List of class names for which member attributes should not be checked (useful 593 | # for classes with dynamically set attributes). This supports the use of 594 | # qualified names. 595 | ignored-classes = ["optparse.Values", "thread._local", "_thread._local", "argparse.Namespace"] 596 | 597 | # Show a hint with possible names when a member name was not found. The aspect of 598 | # finding the hint is based on edit distance. 599 | missing-member-hint = true 600 | 601 | # The minimum edit distance a name should have in order to be considered a 602 | # similar match for a missing member name. 603 | missing-member-hint-distance = 1 604 | 605 | # The total number of similar names that should be taken in consideration when 606 | # showing a hint for a missing member. 607 | missing-member-max-choices = 1 608 | 609 | # Regex pattern to define which classes are considered mixins. 610 | mixin-class-rgx = ".*[Mm]ixin" 611 | 612 | # List of decorators that change the signature of a decorated function. 613 | # signature-mutators = 614 | 615 | [tool.pylint.variables] 616 | # List of additional names supposed to be defined in builtins. Remember that you 617 | # should avoid defining new builtins when possible. 618 | # additional-builtins = 619 | 620 | # Tells whether unused global variables should be treated as a violation. 621 | allow-global-unused-variables = true 622 | 623 | # List of names allowed to shadow builtins 624 | # allowed-redefined-builtins = 625 | 626 | # List of strings which can identify a callback function by name. A callback name 627 | # must start or end with one of those strings. 628 | callbacks = ["cb_", "_cb"] 629 | 630 | # A regular expression matching the name of dummy variables (i.e. expected to not 631 | # be used). 632 | dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" 633 | 634 | # Argument names that match this expression will be ignored. 635 | ignored-argument-names = "_.*|^ignored_|^unused_" 636 | 637 | # Tells whether we should check for unused import in __init__ files. 638 | # init-import = 639 | 640 | # List of qualified module names which can have objects that can redefine 641 | # builtins. 642 | redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"] 643 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "astroid" 5 | version = "3.3.10" 6 | description = "An abstract syntax tree for Python with inference support." 7 | optional = false 8 | python-versions = ">=3.9.0" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb"}, 12 | {file = "astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce"}, 13 | ] 14 | 15 | [package.dependencies] 16 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 17 | 18 | [[package]] 19 | name = "autopep8" 20 | version = "2.3.2" 21 | description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" 22 | optional = false 23 | python-versions = ">=3.9" 24 | groups = ["dev"] 25 | files = [ 26 | {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, 27 | {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, 28 | ] 29 | 30 | [package.dependencies] 31 | pycodestyle = ">=2.12.0" 32 | tomli = {version = "*", markers = "python_version < \"3.11\""} 33 | 34 | [[package]] 35 | name = "certifi" 36 | version = "2025.4.26" 37 | description = "Python package for providing Mozilla's CA Bundle." 38 | optional = false 39 | python-versions = ">=3.6" 40 | groups = ["main"] 41 | files = [ 42 | {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, 43 | {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, 44 | ] 45 | 46 | [[package]] 47 | name = "charset-normalizer" 48 | version = "3.4.2" 49 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 50 | optional = false 51 | python-versions = ">=3.7" 52 | groups = ["main"] 53 | files = [ 54 | {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, 55 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, 56 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, 57 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, 58 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, 59 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, 60 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, 61 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, 62 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, 63 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, 64 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, 65 | {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, 66 | {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, 67 | {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, 68 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, 69 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, 70 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, 71 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, 72 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, 73 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, 74 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, 75 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, 76 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, 77 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, 78 | {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, 79 | {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, 80 | {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, 81 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, 82 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, 83 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, 84 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, 85 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, 86 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, 87 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, 88 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, 89 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, 90 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, 91 | {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, 92 | {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, 93 | {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, 94 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, 95 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, 96 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, 97 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, 98 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, 99 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, 100 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, 101 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, 102 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, 103 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, 104 | {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, 105 | {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, 106 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, 107 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, 108 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, 109 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, 110 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, 111 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, 112 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, 113 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, 114 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, 115 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, 116 | {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, 117 | {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, 118 | {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, 119 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, 120 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, 121 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, 122 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, 123 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, 124 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, 125 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, 126 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, 127 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, 128 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, 129 | {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, 130 | {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, 131 | {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, 132 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, 133 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, 134 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, 135 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, 136 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, 137 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, 138 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, 139 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, 140 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, 141 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, 142 | {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, 143 | {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, 144 | {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, 145 | {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, 146 | ] 147 | 148 | [[package]] 149 | name = "colorama" 150 | version = "0.4.6" 151 | description = "Cross-platform colored terminal text." 152 | optional = false 153 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 154 | groups = ["dev"] 155 | markers = "sys_platform == \"win32\"" 156 | files = [ 157 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 158 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 159 | ] 160 | 161 | [[package]] 162 | name = "coverage" 163 | version = "7.8.0" 164 | description = "Code coverage measurement for Python" 165 | optional = false 166 | python-versions = ">=3.9" 167 | groups = ["dev"] 168 | files = [ 169 | {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, 170 | {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, 171 | {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, 172 | {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, 173 | {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, 174 | {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, 175 | {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, 176 | {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, 177 | {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, 178 | {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, 179 | {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, 180 | {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, 181 | {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, 182 | {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, 183 | {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, 184 | {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, 185 | {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, 186 | {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, 187 | {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, 188 | {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, 189 | {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, 190 | {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, 191 | {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, 192 | {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, 193 | {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, 194 | {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, 195 | {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, 196 | {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, 197 | {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, 198 | {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, 199 | {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, 200 | {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, 201 | {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, 202 | {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, 203 | {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, 204 | {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, 205 | {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, 206 | {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, 207 | {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, 208 | {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, 209 | {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, 210 | {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, 211 | {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, 212 | {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, 213 | {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, 214 | {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, 215 | {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, 216 | {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, 217 | {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, 218 | {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, 219 | {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, 220 | {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, 221 | {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, 222 | {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, 223 | {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, 224 | {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, 225 | {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, 226 | {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, 227 | {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, 228 | {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, 229 | {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, 230 | {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, 231 | {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, 232 | ] 233 | 234 | [package.dependencies] 235 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 236 | 237 | [package.extras] 238 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 239 | 240 | [[package]] 241 | name = "dill" 242 | version = "0.4.0" 243 | description = "serialize all of Python" 244 | optional = false 245 | python-versions = ">=3.8" 246 | groups = ["dev"] 247 | files = [ 248 | {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, 249 | {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, 250 | ] 251 | 252 | [package.extras] 253 | graph = ["objgraph (>=1.7.2)"] 254 | profile = ["gprof2dot (>=2022.7.29)"] 255 | 256 | [[package]] 257 | name = "exceptiongroup" 258 | version = "1.3.0" 259 | description = "Backport of PEP 654 (exception groups)" 260 | optional = false 261 | python-versions = ">=3.7" 262 | groups = ["dev"] 263 | markers = "python_version < \"3.11\"" 264 | files = [ 265 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 266 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 267 | ] 268 | 269 | [package.dependencies] 270 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 271 | 272 | [package.extras] 273 | test = ["pytest (>=6)"] 274 | 275 | [[package]] 276 | name = "idna" 277 | version = "3.10" 278 | description = "Internationalized Domain Names in Applications (IDNA)" 279 | optional = false 280 | python-versions = ">=3.6" 281 | groups = ["main"] 282 | files = [ 283 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 284 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 285 | ] 286 | 287 | [package.extras] 288 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 289 | 290 | [[package]] 291 | name = "iniconfig" 292 | version = "2.1.0" 293 | description = "brain-dead simple config-ini parsing" 294 | optional = false 295 | python-versions = ">=3.8" 296 | groups = ["dev"] 297 | files = [ 298 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 299 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 300 | ] 301 | 302 | [[package]] 303 | name = "isort" 304 | version = "6.0.1" 305 | description = "A Python utility / library to sort Python imports." 306 | optional = false 307 | python-versions = ">=3.9.0" 308 | groups = ["dev"] 309 | files = [ 310 | {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, 311 | {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, 312 | ] 313 | 314 | [package.extras] 315 | colors = ["colorama"] 316 | plugins = ["setuptools"] 317 | 318 | [[package]] 319 | name = "mccabe" 320 | version = "0.7.0" 321 | description = "McCabe checker, plugin for flake8" 322 | optional = false 323 | python-versions = ">=3.6" 324 | groups = ["dev"] 325 | files = [ 326 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 327 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 328 | ] 329 | 330 | [[package]] 331 | name = "opencc" 332 | version = "1.1.9" 333 | description = "Conversion between Traditional and Simplified Chinese" 334 | optional = true 335 | python-versions = "*" 336 | groups = ["main"] 337 | markers = "extra == \"opencc\"" 338 | files = [ 339 | {file = "OpenCC-1.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a33941dd4cb67457e6f44dfe36dddc30a602363a4f6a29b41d79b062b332c094"}, 340 | {file = "OpenCC-1.1.9-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:92769f9a60301574c73096f9ab8a9060fe0d13a9f8266735d82a2a3a92adbd26"}, 341 | {file = "OpenCC-1.1.9-cp310-cp310-win_amd64.whl", hash = "sha256:84e35e5ecfad445a64c0dcd6567d9e9f3a6aed9a6ffd89cdbc071f36cb9e089e"}, 342 | {file = "OpenCC-1.1.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3fb7c84f7c182cb5208e7bc1c104b817a3ca1a8fe111d4d19816be0d6e1ab396"}, 343 | {file = "OpenCC-1.1.9-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:64994c68796d93cdba42f37e0c073fb8ed6f9d6707232be0ba84f24dc5a36bbb"}, 344 | {file = "OpenCC-1.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:9f6a1413ca2ff490e65a55822e4cae8c3f104bfab46355288de4893a14470fbb"}, 345 | {file = "OpenCC-1.1.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48bc3e37942b91a9cf51f525631792f79378e5332bdba9e10c05f6e7fe9036ca"}, 346 | {file = "OpenCC-1.1.9-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:1c5d1489bdaf9dc2865f0ea30eb565093253e73c1868d9c19554c8a044b545d4"}, 347 | {file = "OpenCC-1.1.9-cp312-cp312-win_amd64.whl", hash = "sha256:64f8d22c8505b65e8ee2d6e73241cbc92785d38b3c93885b423d7c4fcd31c679"}, 348 | {file = "OpenCC-1.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4267b66ed6e656b5d8199f94e9673950ac39d49ebaf0e7927330801f06f038f"}, 349 | {file = "OpenCC-1.1.9-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:c6d5f9756ed08e67de36c53dc4d8f0bdc72889d6f57a8fc4d8b073d99c58d4dc"}, 350 | {file = "OpenCC-1.1.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6c2650bd3d6a9e3c31fc2057e0f36122c9507af1661627542f618c97d420293"}, 351 | {file = "OpenCC-1.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d66473405c2e360ef346fe1625f201f3f3c4adbb16d5c1c7749a150ae42d875"}, 352 | {file = "OpenCC-1.1.9-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:436c43e0855b4f9c9e4fd1191e8ac638e9d9f2c7e2d5753952e6e31aa231d36c"}, 353 | {file = "OpenCC-1.1.9-cp39-cp39-win_amd64.whl", hash = "sha256:b4c36d6974afd94b444ad5ad17364f40d228092ce89b86e46653f7ff38075201"}, 354 | {file = "opencc-1.1.9.tar.gz", hash = "sha256:8ad72283732951303390fae33a1ceda98ac9b03368a8f2912edc934d74077e4a"}, 355 | ] 356 | 357 | [[package]] 358 | name = "packaging" 359 | version = "25.0" 360 | description = "Core utilities for Python packages" 361 | optional = false 362 | python-versions = ">=3.8" 363 | groups = ["dev"] 364 | files = [ 365 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 366 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 367 | ] 368 | 369 | [[package]] 370 | name = "platformdirs" 371 | version = "4.3.8" 372 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 373 | optional = false 374 | python-versions = ">=3.9" 375 | groups = ["dev"] 376 | files = [ 377 | {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, 378 | {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, 379 | ] 380 | 381 | [package.extras] 382 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 383 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 384 | type = ["mypy (>=1.14.1)"] 385 | 386 | [[package]] 387 | name = "pluggy" 388 | version = "1.6.0" 389 | description = "plugin and hook calling mechanisms for python" 390 | optional = false 391 | python-versions = ">=3.9" 392 | groups = ["dev"] 393 | files = [ 394 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 395 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 396 | ] 397 | 398 | [package.extras] 399 | dev = ["pre-commit", "tox"] 400 | testing = ["coverage", "pytest", "pytest-benchmark"] 401 | 402 | [[package]] 403 | name = "pycodestyle" 404 | version = "2.13.0" 405 | description = "Python style guide checker" 406 | optional = false 407 | python-versions = ">=3.9" 408 | groups = ["dev"] 409 | files = [ 410 | {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, 411 | {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, 412 | ] 413 | 414 | [[package]] 415 | name = "pylint" 416 | version = "3.3.7" 417 | description = "python code static checker" 418 | optional = false 419 | python-versions = ">=3.9.0" 420 | groups = ["dev"] 421 | files = [ 422 | {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, 423 | {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, 424 | ] 425 | 426 | [package.dependencies] 427 | astroid = ">=3.3.8,<=3.4.0.dev0" 428 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 429 | dill = [ 430 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 431 | {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, 432 | {version = ">=0.3.6", markers = "python_version == \"3.11\""}, 433 | ] 434 | isort = ">=4.2.5,<5.13 || >5.13,<7" 435 | mccabe = ">=0.6,<0.8" 436 | platformdirs = ">=2.2" 437 | tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} 438 | tomlkit = ">=0.10.1" 439 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 440 | 441 | [package.extras] 442 | spelling = ["pyenchant (>=3.2,<4.0)"] 443 | testutils = ["gitpython (>3)"] 444 | 445 | [[package]] 446 | name = "pypinyin" 447 | version = "0.54.0" 448 | description = "汉字拼音转换模块/工具." 449 | optional = false 450 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,<4,>=2.6" 451 | groups = ["main"] 452 | files = [ 453 | {file = "pypinyin-0.54.0-py2.py3-none-any.whl", hash = "sha256:5f776f19b9fd922e4121a114810b22048d90e6e8037fb1c07f4c40f987ae6e7a"}, 454 | {file = "pypinyin-0.54.0.tar.gz", hash = "sha256:9ab0d07ff51d191529e22134a60e109d0526d80b7a80afa73da4c89521610958"}, 455 | ] 456 | 457 | [[package]] 458 | name = "pytest" 459 | version = "8.3.5" 460 | description = "pytest: simple powerful testing with Python" 461 | optional = false 462 | python-versions = ">=3.8" 463 | groups = ["dev"] 464 | files = [ 465 | {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, 466 | {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, 467 | ] 468 | 469 | [package.dependencies] 470 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 471 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 472 | iniconfig = "*" 473 | packaging = "*" 474 | pluggy = ">=1.5,<2" 475 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 476 | 477 | [package.extras] 478 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 479 | 480 | [[package]] 481 | name = "pytest-cov" 482 | version = "5.0.0" 483 | description = "Pytest plugin for measuring coverage." 484 | optional = false 485 | python-versions = ">=3.8" 486 | groups = ["dev"] 487 | files = [ 488 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 489 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 490 | ] 491 | 492 | [package.dependencies] 493 | coverage = {version = ">=5.2.1", extras = ["toml"]} 494 | pytest = ">=4.6" 495 | 496 | [package.extras] 497 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 498 | 499 | [[package]] 500 | name = "pyyaml" 501 | version = "6.0.2" 502 | description = "YAML parser and emitter for Python" 503 | optional = false 504 | python-versions = ">=3.8" 505 | groups = ["main"] 506 | files = [ 507 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 508 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 509 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 510 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 511 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 512 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 513 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 514 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 515 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 516 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 517 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 518 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 519 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 520 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 521 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 522 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 523 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 524 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 525 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 526 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 527 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 528 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 529 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 530 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 531 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 532 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 533 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 534 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 535 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 536 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 537 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 538 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 539 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 540 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 541 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 542 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 543 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 544 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 545 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 546 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 547 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 548 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 549 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 550 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 551 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 552 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 553 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 554 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 555 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 556 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 557 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 558 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 559 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 560 | ] 561 | 562 | [[package]] 563 | name = "requests" 564 | version = "2.32.4" 565 | description = "Python HTTP for Humans." 566 | optional = false 567 | python-versions = ">=3.8" 568 | groups = ["main"] 569 | files = [ 570 | {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, 571 | {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, 572 | ] 573 | 574 | [package.dependencies] 575 | certifi = ">=2017.4.17" 576 | charset_normalizer = ">=2,<4" 577 | idna = ">=2.5,<4" 578 | urllib3 = ">=1.21.1,<3" 579 | 580 | [package.extras] 581 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 582 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 583 | 584 | [[package]] 585 | name = "tomli" 586 | version = "2.2.1" 587 | description = "A lil' TOML parser" 588 | optional = false 589 | python-versions = ">=3.8" 590 | groups = ["dev"] 591 | markers = "python_version < \"3.11\"" 592 | files = [ 593 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 594 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 595 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 596 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 597 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 598 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 599 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 600 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 601 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 602 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 603 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 604 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 605 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 606 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 607 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 608 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 609 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 610 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 611 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 612 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 613 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 614 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 615 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 616 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 617 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 618 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 619 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 620 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 621 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 622 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 623 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 624 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 625 | ] 626 | 627 | [[package]] 628 | name = "tomlkit" 629 | version = "0.13.2" 630 | description = "Style preserving TOML library" 631 | optional = false 632 | python-versions = ">=3.8" 633 | groups = ["dev"] 634 | files = [ 635 | {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, 636 | {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, 637 | ] 638 | 639 | [[package]] 640 | name = "typing-extensions" 641 | version = "4.13.2" 642 | description = "Backported and Experimental Type Hints for Python 3.8+" 643 | optional = false 644 | python-versions = ">=3.8" 645 | groups = ["dev"] 646 | markers = "python_version < \"3.11\"" 647 | files = [ 648 | {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, 649 | {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, 650 | ] 651 | 652 | [[package]] 653 | name = "urllib3" 654 | version = "2.6.0" 655 | description = "HTTP library with thread-safe connection pooling, file post, and more." 656 | optional = false 657 | python-versions = ">=3.9" 658 | groups = ["main"] 659 | files = [ 660 | {file = "urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f"}, 661 | {file = "urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1"}, 662 | ] 663 | 664 | [package.extras] 665 | brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] 666 | h2 = ["h2 (>=4,<5)"] 667 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 668 | zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] 669 | 670 | [extras] 671 | opencc = ["OpenCC"] 672 | 673 | [metadata] 674 | lock-version = "2.1" 675 | python-versions = ">=3.9,<4.0" 676 | content-hash = "d35a0334e8166c87b85e14945c00adf0a4629b93fcf77906727e90df608368ca" 677 | --------------------------------------------------------------------------------