├── furiganalyse ├── __init__.py ├── params.py ├── txt_format.py ├── apkg_format.py ├── epub_format.py ├── templates │ ├── download.html │ └── upload.html ├── __main__.py ├── parsing.py └── app.py ├── assets ├── styles.css ├── spinner.gif └── furiganalyse.jpg ├── docker-compose.yml ├── Dockerrun.aws.json ├── pyproject.toml ├── LICENSE ├── Dockerfile ├── README.md ├── tests └── test_furiganalyse.py └── poetry.lock /furiganalyse/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-align: center; 3 | }; -------------------------------------------------------------------------------- /assets/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsupera/furiganalyse/HEAD/assets/spinner.gif -------------------------------------------------------------------------------- /assets/furiganalyse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsupera/furiganalyse/HEAD/assets/furiganalyse.jpg -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | furiganalyse: 3 | image: furiganalyse:latest 4 | build: . 5 | ports: 6 | - "5000:5000" 7 | volumes: 8 | - .:/workdir -------------------------------------------------------------------------------- /Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Image": { 4 | "Name": "itsupera/furiganalyse" 5 | }, 6 | "Ports": [ 7 | { 8 | "ContainerPort": "5000" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /furiganalyse/params.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class FuriganaMode(str, Enum): 5 | add = "add" 6 | replace = "replace" 7 | remove = "remove" 8 | 9 | 10 | class OutputFormat(str, Enum): 11 | epub = "epub" 12 | mobi = "mobi" 13 | azw3 = "azw3" 14 | many_txt = "many_txt" 15 | single_txt = "single_txt" 16 | apkg = "apkg" 17 | html = "html" 18 | 19 | 20 | class WritingMode(str, Enum): 21 | horizontal_tb = "horizontal-tb" 22 | vertical_rl = "vertical-rl" 23 | vertical_lr = "vertical-lr" -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "furiganalyse" 3 | version = "0.7.6" 4 | description = "Annotate Japanese ebooks with furigana" 5 | authors = ["Itsupera "] 6 | license = "MIT license" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.8" 11 | typer = "^0.3.2" 12 | fastapi = "^0.79.0" 13 | uvicorn = "^0.18.2" 14 | python-multipart = "^0.0.5" 15 | Jinja2 = "^3.1.2" 16 | furigana = { git = "https://github.com/itsupera/furigana.git", tag = "0.4" } 17 | genanki = { git = "https://github.com/kerrickstaley/genanki", tag="v0.13.0" } 18 | pypandoc = "^1.6.4" 19 | capybre = "^0.0.6" 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | ruff = "^0.5.1" 23 | pytest = "^8.2.2" 24 | 25 | [build-system] 26 | requires = ["poetry-core"] 27 | build-backend = "poetry.core.masonry.api" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Itsupera 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-bookworm 2 | LABEL org.opencontainers.image.authors="itsupera@gmail.com" 3 | 4 | # switch to root user to use apt-get 5 | USER root 6 | 7 | # Install mecab and mecab-ipadic from Debian packages 8 | RUN apt-get update && apt-get install -y \ 9 | git curl file python3-poetry \ 10 | mecab=0.996-14+b14 mecab-ipadic=2.7.0-20070801+main-3 mecab-ipadic-utf8=2.7.0-20070801+main-3 libmecab-dev=0.996-14+b14 \ 11 | sudo \ 12 | pandoc calibre \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # NEologd 16 | RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git 17 | RUN cd mecab-ipadic-neologd && ./bin/install-mecab-ipadic-neologd -n -a -y 18 | RUN rm -rf mecab-ipadic-neologd 19 | 20 | # Move config file from /etc/mecabrc (default install path on Debian) to what the program expects 21 | # Setup MeCab to use mecab-ipadic-neologd dict by default 22 | RUN cp /etc/mecabrc /usr/local/etc/mecabrc && \ 23 | sed -i "s'^dicdir.*'dicdir = /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd'g" /usr/local/etc/mecabrc 24 | 25 | # Setup our dependencies 26 | WORKDIR /workdir 27 | ADD README.md . 28 | ADD pyproject.toml . 29 | ADD poetry.lock . 30 | # Little optimization in order to install our dependencies before copying the source 31 | RUN mkdir furiganalyse && touch furiganalyse/__init__.py 32 | RUN pip3 install -e . 33 | 34 | # Add the sources 35 | ADD furiganalyse furiganalyse 36 | ADD assets assets 37 | 38 | EXPOSE 5000 39 | 40 | ENTRYPOINT ["uvicorn", "furiganalyse.app:app", "--workers", "10", "--host", "0.0.0.0", "--port", "5000"] 41 | -------------------------------------------------------------------------------- /furiganalyse/txt_format.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import zipfile 4 | from typing import Iterator 5 | 6 | 7 | def write_txt_archive(unzipped_input_fpath: str, outputfile: str): 8 | """ 9 | Only keep the converted txt files within the archive. 10 | """ 11 | with zipfile.ZipFile(outputfile, 'w') as zip_out: 12 | for folder_name, _, filenames in os.walk(unzipped_input_fpath): 13 | for filename in filenames: 14 | extension = os.path.splitext(filename)[1] 15 | if extension == ".txt": 16 | rel_dir = os.path.relpath(folder_name, unzipped_input_fpath) 17 | rel_file = os.path.join(rel_dir, filename) 18 | file_path = os.path.join(folder_name, filename) 19 | # Add file to zip 20 | zip_out.write(file_path, rel_file) 21 | logging.info(f" Adding {rel_file}") 22 | 23 | 24 | def iter_txt_lines(unzipped_input_fpath: str) -> Iterator[str]: 25 | for root, _, files in sorted(os.walk(unzipped_input_fpath)): 26 | for file in sorted(files): 27 | if os.path.splitext(file)[1] == ".txt": 28 | filepath = os.path.join(root, file) 29 | with open(filepath) as fd: 30 | for line in fd: 31 | yield line 32 | 33 | 34 | def concat_txt_files(unzipped_input_fpath: str, outputfile: str): 35 | with open(outputfile, "w") as fd: 36 | for line in iter_txt_lines(unzipped_input_fpath): 37 | fd.write(line) -------------------------------------------------------------------------------- /furiganalyse/apkg_format.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Iterator 3 | 4 | import genanki 5 | 6 | from furiganalyse.txt_format import iter_txt_lines 7 | 8 | 9 | def generate_anki_deck(unzipped_input_fpath: str, deck_name: str, anki_deck_filepath: str): 10 | model_id = random.randrange(1 << 30, 1 << 31) 11 | model = genanki.Model( 12 | model_id, 13 | 'Sentence cards (furiganalyse)', 14 | fields=[ 15 | {'name': 'Sentence'}, 16 | {'name': 'Word'}, 17 | {'name': 'Definition'}, 18 | ], 19 | templates=[ 20 | { 21 | 'name': 'Sentence to Word Definition', 22 | 'qfmt': '{{Sentence}}', 23 | 'afmt': '{{FrontSide}}
{{Word}}
{{Definition}}', 24 | }, 25 | ]) 26 | 27 | deck_id = random.randrange(1 << 30, 1 << 31) 28 | description = 'Deck generated with furiganalyse
' \ 29 | 'If you can, please consider donating ' \ 30 | 'to support my work, thank you !' 31 | deck = genanki.Deck( 32 | deck_id, 33 | deck_name, 34 | description 35 | ) 36 | 37 | package = genanki.Package(deck) 38 | package.media_files = [] 39 | 40 | idx = 0 41 | for line in iter_txt_lines(unzipped_input_fpath): 42 | for sentence in extract_sentences(line): 43 | note = genanki.Note( 44 | model=model, 45 | fields=[sentence, "", ""], 46 | due=idx, 47 | ) 48 | deck.add_note(note) 49 | idx += 1 50 | 51 | package.write_to_file(anki_deck_filepath) 52 | 53 | 54 | def extract_sentences(line: str) -> Iterator[str]: 55 | sentences = line.split("。") 56 | for sentence in sentences: 57 | sentence = sentence.lstrip().rstrip() 58 | if sentence: 59 | yield sentence -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Buy Me A Coffee donate button 3 | 4 | 5 | 6 | 7 | 8 | Furiganalyse 9 | ============= 10 | 11 | Annotate Japanese ebooks with furigana, and other conversions. 12 | 13 | → Try it here! 14 | 15 | ![](assets/furiganalyse.jpg) 16 | 17 | --- 18 | 19 | Supported input formats: 20 | - EPUB 21 | - AZW3 (without DRM) 22 | - MOBI 23 | 24 | Supported output formats: 25 | - EPUB 26 | - AZW3 (without DRM) 27 | - MOBI 28 | - Many text files (one per book part) 29 | - Single text file 30 | - Anki Deck (each sentence as a card) 31 | - HTML (readable in web browser) 32 | 33 | Setup and run 34 | -------------- 35 | 36 | Using Docker to create a container with all the dependencies and dictionaries (tested on Ubuntu 24.04): 37 | ```bash 38 | docker compose build 39 | ``` 40 | Or grab the latest prebuilt image: 41 | ```bash 42 | docker pull itsupera/furiganalyse:latest 43 | docker tag itsupera/furiganalyse:latest furiganalyse:latest 44 | ``` 45 | 46 | ### Run as a web app 47 | ```bash 48 | docker compose up -d 49 | ``` 50 | Then open http://127.0.0.1:5000 in your web browser 51 | 52 | ### Run as a CLI 53 | ```bash 54 | # Run this from the directory your ebook (for example "book.epub") is in 55 | docker run -v $PWD:/workspace --entrypoint=python3 furiganalyse:latest \ 56 | -m furiganalyse /workspace/book.epub /workspace/book_with_furigana.epub 57 | ``` 58 | 59 | ### Calling the API 60 | ```bash 61 | # Submit a job 62 | curl -v -XPOST http://127.0.0.1/submit \ 63 | -F "file=@" \ 64 | -F furigana_mode="add" \ 65 | -F writing_mode="horizontal-tb" \ 66 | -F of="epub" \ 67 | -F redirect=false 68 | 69 | # Response will look like this: 70 | # {"uid":""} 71 | 72 | # Check the status of the job 73 | curl -v http://127.0.0.1/jobs//status 74 | # Response will look like this: 75 | # { 76 | # "uid": "", 77 | # "status": "complete", 78 | # "result": "(...data...)" 79 | # } 80 | 81 | # Download the result 82 | curl http://127.0.0.1/jobs//file -o output.epub 83 | ``` 84 | 85 | Local development setup 86 | ------------------------ 87 | 88 | Install python and poetry, (optionally) create a virtual environment, and install the dependencies: 89 | ```bash 90 | poetry install 91 | ``` 92 | -------------------------------------------------------------------------------- /furiganalyse/epub_format.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import zipfile 5 | from pathlib import Path 6 | 7 | from furiganalyse.params import OutputFormat, WritingMode 8 | from furiganalyse.parsing import process_html, convert_html_to_txt 9 | 10 | 11 | def process_epub_file(unzipped_input_fpath, mode, writing_mode, output_format): 12 | if writing_mode is not None: 13 | update_writing_mode(unzipped_input_fpath, writing_mode) 14 | 15 | for root, _, files in os.walk(unzipped_input_fpath): 16 | for file in files: 17 | if os.path.splitext(file)[1] in {".html", ".xhtml"}: 18 | logging.info(f" Processing {file}") 19 | html_filepath = os.path.join(root, file) 20 | tree = process_html(html_filepath, mode) 21 | if output_format in {OutputFormat.many_txt, OutputFormat.single_txt, OutputFormat.apkg}: 22 | txt_outputfile = os.path.splitext(html_filepath)[0] + '.txt' 23 | convert_html_to_txt(tree, txt_outputfile) 24 | else: 25 | tree.write(html_filepath, encoding="utf-8") 26 | 27 | 28 | def update_writing_mode(unzipped_input_fpath: str, writing_mode: WritingMode): 29 | for css_filepath in Path(unzipped_input_fpath).glob('**/*.css'): 30 | with open(css_filepath) as fd: 31 | css_content = fd.read() 32 | 33 | pattern = re.compile(r"-webkit-writing-mode: [^;\n]+") 34 | css_content = pattern.sub(f"-webkit-writing-mode: {writing_mode}", css_content) 35 | 36 | with open(css_filepath, "w") as fd: 37 | fd.write(css_content) 38 | 39 | # content.opf has a tag like this: 40 | # content_opf_path: Path = Path(unzipped_input_fpath) / "content.opf" 41 | # if content_opf_path.exists(): 42 | # from xml.etree import ElementTree as ET 43 | # tree = ET.parse(content_opf_path) 44 | # # import ipdb; ipdb.set_trace() 45 | # x: ET.Element = tree.find(".//{http://www.idpf.org/2007/opf}meta[@name='primary-writing-mode']") 46 | # x.attrib["content"] = writing_mode.value 47 | # tree.write(content_opf_path, encoding="utf-8") 48 | 49 | 50 | def write_epub_archive(unzipped_input_fpath: str, outputfile: str): 51 | """ 52 | Write the modified extracted EPUB archive to a new archive file. 53 | """ 54 | with zipfile.ZipFile(outputfile, 'w') as zip_out: 55 | for folder_name, _, filenames in os.walk(unzipped_input_fpath): 56 | for filename in filenames: 57 | rel_dir = os.path.relpath(folder_name, unzipped_input_fpath) 58 | rel_file = os.path.join(rel_dir, filename) 59 | file_path = os.path.join(folder_name, filename) 60 | # Add file to zip 61 | zip_out.write(file_path, rel_file) 62 | logging.info(f" Adding {rel_file}") -------------------------------------------------------------------------------- /furiganalyse/templates/download.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Furiganalyse 4 | 5 | 6 | 7 | 41 | 42 | 43 | 44 | 45 |
46 |

Converting your ebook...

47 |
48 |
49 |
50 |
51 | 68 | 78 | -------------------------------------------------------------------------------- /furiganalyse/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import zipfile 4 | from tempfile import TemporaryDirectory 5 | from typing import Optional 6 | 7 | import capybre 8 | import pypandoc 9 | import typer 10 | 11 | from furiganalyse.apkg_format import generate_anki_deck 12 | from furiganalyse.epub_format import process_epub_file, write_epub_archive 13 | from furiganalyse.params import FuriganaMode, OutputFormat, WritingMode 14 | from furiganalyse.txt_format import write_txt_archive, concat_txt_files 15 | 16 | logging.basicConfig(level=logging.INFO) 17 | 18 | SUPPORTED_INPUT_EXTS = {".epub", ".azw3", ".mobi", ".txt", ".html"} 19 | 20 | def main( 21 | inputfile: str, 22 | outputfile: str, 23 | furigana_mode: FuriganaMode = FuriganaMode.add, 24 | output_format: OutputFormat = OutputFormat.epub, 25 | writing_mode: Optional[WritingMode] = None, 26 | ): 27 | with TemporaryDirectory() as td: 28 | filename, ext = os.path.splitext(os.path.basename(inputfile)) 29 | inputfile = convert_inputfile_if_not_epub(inputfile, ext, td) 30 | 31 | unzipped_input_fpath = os.path.join(td, "unzipped") 32 | 33 | logging.info("Extracting the archive ...") 34 | with zipfile.ZipFile(inputfile, 'r') as zip_ref: 35 | zip_ref.extractall(unzipped_input_fpath) 36 | 37 | logging.info("Processing the files ...") 38 | process_epub_file(unzipped_input_fpath, furigana_mode, writing_mode, output_format) 39 | 40 | logging.info("Creating the output file ...") 41 | if output_format == OutputFormat.epub: 42 | write_epub_archive(unzipped_input_fpath, outputfile) 43 | elif output_format in {OutputFormat.mobi, OutputFormat.azw3}: 44 | tmpfilepath = os.path.join(td, "tmp.epub") 45 | write_epub_archive(unzipped_input_fpath, tmpfilepath) 46 | capybre.convert(tmpfilepath, outputfile, as_ext=output_format.value, suppress_output=False) 47 | elif output_format == OutputFormat.many_txt: 48 | write_txt_archive(unzipped_input_fpath, outputfile) 49 | elif output_format == OutputFormat.single_txt: 50 | concat_txt_files(unzipped_input_fpath, outputfile) 51 | elif output_format == OutputFormat.apkg: 52 | deck_name = filename 53 | generate_anki_deck(unzipped_input_fpath, deck_name, outputfile) 54 | elif output_format == OutputFormat.html: 55 | tmpfilepath = os.path.join(td, "tmp.epub") 56 | write_epub_archive(unzipped_input_fpath, tmpfilepath) 57 | pypandoc.convert_file(tmpfilepath, 'html', outputfile=outputfile) 58 | else: 59 | raise ValueError("Invalid writing mode") 60 | 61 | 62 | def convert_inputfile_if_not_epub(inputfile, ext, td): 63 | """ 64 | Convert the input file to EPUB if it's not already in that format 65 | :param inputfile: file path to the input file 66 | :param ext: extension of the input file (e.g. ".html") 67 | :param td: temporary directory path to store the converted file 68 | :return: path to the converted file, or the original file if it's already in EPUB format 69 | """ 70 | if ext not in SUPPORTED_INPUT_EXTS: 71 | raise Exception(f"Extension {ext} is not supported, input file format must be one of these: " 72 | f"{','.join(SUPPORTED_INPUT_EXTS)}") 73 | 74 | if ext == ".epub": 75 | return inputfile 76 | 77 | logging.info(f"Convert {ext.lstrip('.').upper()} to EPUB first ...") 78 | tmpfilepath = os.path.join(td, "tmp.epub") 79 | if ext == ".html": 80 | pypandoc.convert_file(inputfile, 'epub', outputfile=tmpfilepath) 81 | else: 82 | capybre.convert(inputfile, tmpfilepath, as_ext='epub', suppress_output=False) 83 | return tmpfilepath 84 | 85 | 86 | if __name__ == '__main__': 87 | typer.run(main) 88 | -------------------------------------------------------------------------------- /tests/test_furiganalyse.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import pytest 4 | 5 | from furiganalyse.parsing import process_tree 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ("test_case", "xml_str", "mode", "expected_xml_str"), 10 | [ 11 | ( 12 | "Remove furigana", 13 | 'はじめに、第一ファースト歩。おわり', 14 | "remove", 15 | 'はじめに、第一歩。終', 16 | ), 17 | ( 18 | "Remove furigana, handling rb elements", 19 | "黒い服を着た大人たちの間に", 20 | "remove", 21 | "黒い服を着た大人達の間に", 22 | ), 23 | ( 24 | "Remove furigana, handling rp elements", 25 | "(Kan)(ji)", 26 | "remove", 27 | "漢字", 28 | ), 29 | ( 30 | "Remove furigana, parent node with text", 31 | '第一ファースト', 32 | "remove", 33 | '第一', 34 | ), 35 | ( 36 | "Remove furigana, no text", 37 | 'ファースト', 38 | "remove", 39 | '', 40 | ), 41 | ( 42 | "Remove furigana, no text or childs", 43 | '', 44 | "remove", 45 | '', 46 | ), 47 | ( 48 | "Override furigana", 49 | 'はじめに、第一ファースト歩。おわり', 50 | "replace", 51 | 'はじめに、第一歩だいいっぽおわり', 52 | ), 53 | ( 54 | "Override furigana, handling rb elements", 55 | "大人あああの間に", 56 | "replace", 57 | "大人おとなたちに" 58 | ), 59 | ( 60 | "Text may be positioned before, inside or after elements", 61 | """ 62 | 63 |
64 |

65 |  1つの成功体験は 66 | ハーバード大学。 67 | その真ん中を 68 | はじめに、第一。 69 |

70 | その後で 71 |
72 | 73 | """, 74 | "add", 75 | """ 76 | 77 |
78 |

1つの成功体験せいこうたいけんハーバード大学だいがくそのなかはじめに、第一だいいち 79 |

その
80 | 81 | """, 82 | ), 83 | ( 84 | "Romaji is not modified", 85 | '

No kanji around here

', 86 | "add", 87 | '

No kanji around here

', 88 | ), 89 | ( 90 | "Escaped characters", 91 | '>ファスト&スロー<:'あなた'の意思"は"', 92 | "add", 93 | '>ファスト&スロー<:'あなた'の意思いし"は"', 94 | ), 95 | ( 96 | "Applying the a title tag in the head", 97 | '世界一やさしい「やりたいこと」の見つけ方 人生のモヤモヤから解放される自己理解メソッド', 98 | "add", 99 | '<ruby>世界一<rt>せかいいち</rt></ruby>やさしい「やりたいこと」の<ruby>見<rt>み</rt></ruby>つけ<ruby>方<rt>かた</rt></ruby> <ruby>人生<rt>じんせい</rt></ruby>のモヤモヤから<ruby>解放<rt>かいほう</rt></ruby>される<ruby>自己<rt>じこ</rt></ruby><ruby>理解<rt>りかい</rt></ruby>メソッド', 100 | ), 101 | ( 102 | "Don't override existing furigana", 103 | 'はじめに、第一ファースト歩。', 104 | "add", 105 | 'はじめに、第一ファースト。', 106 | ), 107 | ( 108 | "Tag inside of ruby subtags", 109 | '辿たど三', 110 | "add", 111 | '辿たどさん', 112 | ), 113 | ] 114 | ) 115 | def test_process_tree(test_case, xml_str, mode, expected_xml_str): 116 | template = """ 117 | 118 | 119 | {} 120 | 121 | """.strip() 122 | 123 | tree = ET.fromstring(template.format(xml_str)) 124 | 125 | process_tree(tree, mode) 126 | 127 | expected_tree = ET.fromstring(template.format(expected_xml_str)) 128 | 129 | assert ET.tostring(tree, encoding='unicode') == ET.tostring(expected_tree, encoding='unicode') -------------------------------------------------------------------------------- /furiganalyse/parsing.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from typing import Tuple, List, Iterable 4 | from xml.etree import ElementTree as ET 5 | 6 | from furigana.furigana import create_furigana_html 7 | 8 | from furiganalyse.params import FuriganaMode 9 | 10 | NAMESPACE = "{http://www.w3.org/1999/xhtml}" 11 | 12 | 13 | def process_html(inputfile: str, mode: FuriganaMode) -> ET.ElementTree: 14 | tree = ET.parse(inputfile) 15 | process_tree(tree, mode) 16 | return tree 17 | 18 | 19 | def process_tree(tree: ET.ElementTree, mode: FuriganaMode): 20 | parent_map = dict((c, p) for p in tree.iter() for c in p) 21 | 22 | if mode in {"remove", "replace"}: 23 | remove_existing_furigana(tree, parent_map) 24 | 25 | if mode in {"add", "replace"}: 26 | ps = tree.findall(f'.//{NAMESPACE}*') 27 | for p in ps: 28 | # Exclude ruby related tags, we don't want to override them (unless we have removed them before) 29 | if not inside_ruby_subtag(p, parent_map): 30 | logging.debug(f">>> BEFORE {p.tag} > '{p.text}' {list(p)} '{p.tail}'") 31 | process_head(p) 32 | process_tail(p, parent_map[p]) 33 | logging.debug(f">>> AFTER {p.tag} > '{p.text}' {list(p)} '{p.tail}'") 34 | 35 | # Add the namespace to our new elements 36 | elems = tree.findall('.//{}*') 37 | for elem in elems: 38 | elem.tag = NAMESPACE + elem.tag 39 | 40 | 41 | def inside_ruby_subtag(elem: ET.Element, parent_map): 42 | """ 43 | Returns True if any of the ascendant tags is a ruby subtag (, or ) 44 | """ 45 | while elem is not None: 46 | if any((elem.tag.endswith(tag) for tag in {"rt", "rb", "rp"})): 47 | return True 48 | elem = parent_map.get(elem) 49 | return False 50 | 51 | 52 | def remove_existing_furigana(tree: ET.ElementTree, parent_map: dict): 53 | """ 54 | Replace all existing ruby elements by their text, e.g., XY becomes X. 55 | """ 56 | elems = tree.findall(f'.//{NAMESPACE}ruby') 57 | for elem in elems: 58 | # Remove all the children, e.g., the readings, but keep the text from other childs 59 | childs_text = [] 60 | for child in list(elem): 61 | if not child.tag.endswith("rt") and not child.tag.endswith("rp"): 62 | text = (child.text or "") + (child.tail or "") 63 | else: 64 | text = child.tail or "" 65 | childs_text.append(text) 66 | elem.remove(child) 67 | 68 | # Replacing the node the its text, childs text and tail 69 | new_text = (elem.text or "") + "".join(childs_text) + (elem.tail or "") 70 | 71 | parent_elem = parent_map[elem] 72 | 73 | # Find the previous child to append our new text to it 74 | idx = list(parent_elem).index(elem) 75 | if idx == 0: 76 | # If our element was the first child, append to parent node's text 77 | parent_elem.text = (parent_elem.text or "") + new_text 78 | else: 79 | # Otherwise, append to the tail of previous children 80 | previous_elem = parent_elem[idx - 1] 81 | previous_elem.tail = (previous_elem.tail or "") + new_text 82 | 83 | # Finally, remove our ruby element from its parent 84 | parent_elem.remove(elem) 85 | 86 | 87 | def process_head(elem: ET.Element): 88 | """ 89 | Process the text that is before the children of the given element. 90 | """ 91 | if not elem.text or elem.tag.endswith("ruby"): 92 | return 93 | 94 | text = elem.text.strip() 95 | if contains_kanji(text): 96 | 97 | head, children, tail = create_parsed_furigana_html(text) 98 | 99 | # Replace the original text by the ruby childs "head" 100 | elem.text = head 101 | 102 | # Insert the children at the beginning 103 | for child in reversed(children): 104 | elem.insert(0, child) 105 | 106 | 107 | def process_tail(elem: ET.Element, parent_elem: ET.Element): 108 | """ 109 | Process the text that is before the children of the given element. 110 | """ 111 | if not elem.tail: 112 | return 113 | 114 | text = elem.tail.strip() 115 | if contains_kanji(text): 116 | 117 | head, children, tail = create_parsed_furigana_html(text) 118 | 119 | # Replace the original tail by the rubys "head" 120 | elem.tail = head 121 | 122 | # Insert the ruby children just after the element 123 | idx = list(parent_elem).index(elem) 124 | for child in reversed(children): 125 | parent_elem.insert(idx + 1, child) 126 | 127 | 128 | def create_parsed_furigana_html(text: str) -> Tuple[str, List[ET.Element], str]: 129 | """ 130 | Generate the furigana and return it parsed: "head" text, children, "tail" text. 131 | """ 132 | try: 133 | new_text = create_furigana_html(text) 134 | except Exception: 135 | logging.warning(f"Something wrong happened when retrieving furigana for '{text}'") 136 | new_text = text 137 | 138 | # Need to wrap the children elements in something to parse them 139 | try: 140 | elem = ET.fromstring(f"""

{new_text}

""") 141 | except ET.ParseError: 142 | logging.error(f"XML parsing failed for {new_text}, which was generated from: {text}") 143 | raise 144 | 145 | # Return the parts that will need to be integrated in the XML tree 146 | return elem.text, list(elem), elem.tail 147 | 148 | 149 | kanji_pattern = re.compile(f"[一-龯]") 150 | 151 | 152 | def contains_kanji(text: str) -> bool: 153 | return bool(kanji_pattern.search(text)) 154 | 155 | 156 | def convert_html_to_txt(tree, outputfile): 157 | with open(outputfile, "w") as fd: 158 | for line in convert_html_to_txt_lines(tree): 159 | fd.write(line) 160 | 161 | 162 | def convert_html_to_txt_lines(tree) -> Iterable[str]: 163 | rts = tree.findall(f'.//{NAMESPACE}rt') 164 | for rt in rts: 165 | rt.text = "(" + (rt.text or "") 166 | rt.tail = (rt.tail or "") + ")" 167 | 168 | ps = tree.findall(f'.//{NAMESPACE}p') 169 | for p in ps: 170 | yield ET.tostring(p, encoding="utf-8", method="text").decode("utf-8") -------------------------------------------------------------------------------- /furiganalyse/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import logging 4 | import os 5 | import random 6 | import shutil 7 | import string 8 | import traceback 9 | from concurrent.futures.process import ProcessPoolExecutor 10 | from pathlib import Path 11 | from typing import Dict 12 | from uuid import UUID, uuid4 13 | 14 | from fastapi import BackgroundTasks, Form, FastAPI, Request, status, UploadFile 15 | from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse, Response 16 | from fastapi.staticfiles import StaticFiles 17 | from fastapi.templating import Jinja2Templates 18 | from pydantic import BaseModel, Field 19 | from starlette.middleware.cors import CORSMiddleware 20 | 21 | from furiganalyse.__main__ import main, SUPPORTED_INPUT_EXTS 22 | from furiganalyse.params import OutputFormat, FuriganaMode, WritingMode 23 | 24 | 25 | class Job(BaseModel): 26 | uid: UUID = Field(default_factory=uuid4) 27 | status: str = "in_progress" 28 | result: str = None 29 | 30 | 31 | jobs: Dict[UUID, Job] = {} 32 | 33 | 34 | templates = Jinja2Templates(directory="./furiganalyse/templates") 35 | 36 | # Get the root path from environment variable, default to empty for local development 37 | root_path = os.getenv("ROOT_PATH", "") 38 | app = FastAPI(root_path=root_path) 39 | app.add_middleware( 40 | CORSMiddleware, 41 | allow_origins=[""], 42 | allow_credentials=True, 43 | allow_methods=[""], 44 | allow_headers=["*"], 45 | ) 46 | app.mount("/assets", StaticFiles(directory="assets"), name="assets") 47 | 48 | OUTPUT_FOLDER = '/tmp/furiganalysed/' 49 | Path(OUTPUT_FOLDER).mkdir(exist_ok=True) 50 | 51 | 52 | @app.get("/", response_class=HTMLResponse) 53 | def get_root(request: Request): 54 | return templates.TemplateResponse("upload.html", {"request": request, "supported_input_exts": SUPPORTED_INPUT_EXTS}) 55 | 56 | 57 | @app.post("/submit") 58 | async def task_handler( 59 | background_tasks: BackgroundTasks, 60 | file: UploadFile, 61 | furigana_mode: str = Form(), 62 | writing_mode: str = Form(), 63 | of: str = Form(), 64 | redirect: bool = Form(default=True), 65 | ): 66 | new_task = Job() 67 | jobs[new_task.uid] = new_task 68 | 69 | # Free up some space if necessary 70 | cleanup_output_folder() 71 | 72 | # Write uploaded file to a temporary file 73 | task_folder = os.path.join(OUTPUT_FOLDER, str(new_task.uid)) 74 | Path(task_folder).mkdir(exist_ok=True) 75 | tmpfile = os.path.join(task_folder, file.filename) 76 | contents = file.file.read() 77 | with open(tmpfile, 'wb') as f: 78 | f.write(contents) 79 | 80 | background_tasks.add_task( 81 | start_furiganalyse_task, new_task.uid, task_folder, file.filename, of, furigana_mode, writing_mode 82 | ) 83 | 84 | if redirect: 85 | return RedirectResponse(f"/jobs/{new_task.uid}", status_code=status.HTTP_302_FOUND) 86 | else: 87 | return {"uid": new_task.uid} 88 | 89 | 90 | @app.get("/jobs/{uid}", response_class=HTMLResponse) 91 | def get_download(request: Request, uid: UUID): 92 | return templates.TemplateResponse("download.html", {"request": request, "uid": uid}) 93 | 94 | 95 | def furiganalyse_task( 96 | task_folder: Path, 97 | filename: str, 98 | output_format: str, 99 | furigana_mode: str, 100 | writing_mode: str 101 | ) -> str: 102 | input_filepath = os.path.join(task_folder, filename) 103 | output_filename = generate_output_filename(filename, output_format) 104 | output_filepath = os.path.join(task_folder, output_filename) 105 | path_hash = encode_filepath(output_filepath) 106 | 107 | try: 108 | main( 109 | input_filepath, 110 | output_filepath, 111 | furigana_mode=FuriganaMode(furigana_mode), 112 | output_format=OutputFormat(output_format), 113 | writing_mode=WritingMode(writing_mode), 114 | ) 115 | except Exception: 116 | logging.error(f"Error while processing {input_filepath}: {traceback.format_exc()}") 117 | raise 118 | 119 | return path_hash 120 | 121 | 122 | @app.get("/jobs/{uid}/status") 123 | async def status_handler(uid: UUID): 124 | job = jobs.get(uid) 125 | if not job: 126 | return Response("Uid not found!", status_code=404) 127 | return jobs[uid] 128 | 129 | 130 | @app.get('/jobs/{uid}/file') 131 | def get_file(uid: UUID): 132 | job = jobs.get(uid) 133 | if not job: 134 | return Response("Uid not found!", status_code=404) 135 | 136 | if job.status != "complete": 137 | return Response("Job not completed yet!", status_code=400) 138 | 139 | if not job.result: 140 | return Response("Something went wrong!", status_code=500) 141 | 142 | path_hash = job.result 143 | file_path = decode_filepath(path_hash) 144 | filename = os.path.basename(file_path) 145 | return FileResponse(path=file_path, filename=filename) 146 | 147 | 148 | @app.on_event("startup") 149 | async def startup_event(): 150 | app.state.executor = ProcessPoolExecutor() 151 | 152 | 153 | @app.on_event("shutdown") 154 | async def on_shutdown(): 155 | app.state.executor.shutdown() 156 | 157 | 158 | OUTPUT_FORMAT_TO_EXTENSION = { 159 | OutputFormat.epub: ".epub", 160 | OutputFormat.mobi: ".mobi", 161 | OutputFormat.azw3: ".azw3", 162 | OutputFormat.many_txt: ".zip", 163 | OutputFormat.single_txt: ".txt", 164 | OutputFormat.apkg: ".apkg", 165 | OutputFormat.html: ".html", 166 | } 167 | 168 | 169 | def generate_output_filename(input_filename: str, output_format: OutputFormat) -> str: 170 | filename_without_ext = os.path.splitext(input_filename)[0] 171 | extension = OUTPUT_FORMAT_TO_EXTENSION[output_format] 172 | output_filename = "furiganalysed_" + filename_without_ext + extension 173 | return output_filename 174 | 175 | def generate_random_key(length): 176 | return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length)) 177 | 178 | 179 | def encode_filepath(filepath): 180 | return str(base64.urlsafe_b64encode(filepath.encode("utf-8")), "utf-8") 181 | 182 | 183 | def decode_filepath(hashed_path): 184 | return str(base64.urlsafe_b64decode(hashed_path.encode("utf-8")), "utf-8") 185 | 186 | 187 | def cleanup_output_folder(force: bool = False): 188 | """ 189 | Keep the total size of output folder below a threshold, thrashing from the older files when needed. 190 | """ 191 | size_threshold = int(100e6) # 100MB 192 | 193 | output_folder = Path(OUTPUT_FOLDER) 194 | paths = sorted(output_folder.iterdir(), key=os.path.getctime) 195 | 196 | path_and_sizes = [] 197 | total_size = 0 198 | for path in paths: 199 | size = sum(f.stat().st_size for f in path.glob('**/*') if f.is_file()) 200 | path_and_sizes.append((path, size)) 201 | total_size += size 202 | 203 | if total_size < size_threshold and not force: 204 | return 205 | 206 | for path, size in path_and_sizes: 207 | logging.info(f"Removing {path} to free up space") 208 | 209 | uid = UUID(os.path.basename(path)) 210 | if uid in jobs: 211 | logging.info(f"Deleting associated job {uid}") 212 | del jobs[uid] 213 | 214 | shutil.rmtree(path) 215 | total_size -= size 216 | if total_size < size_threshold and not force: 217 | break 218 | 219 | 220 | async def run_in_process(fn, *args): 221 | loop = asyncio.get_event_loop() 222 | return await loop.run_in_executor(app.state.executor, fn, *args) # wait and return result 223 | 224 | 225 | async def start_furiganalyse_task(uid: UUID, *args) -> None: 226 | try: 227 | jobs[uid].result = await run_in_process(furiganalyse_task, *args) 228 | jobs[uid].status = "complete" 229 | except: 230 | logging.error(f"Error occured for job {uid}") 231 | jobs[uid].status = "error" -------------------------------------------------------------------------------- /furiganalyse/templates/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Furiganalyse 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

📖 Furiganalyse 📖

19 |
20 |
21 |
22 |
23 |
    24 |
  • 25 |

    Select a file (EPUB, AZW3, MOBI, TXT, HTML)

    26 |
    27 | 28 |
    29 |
  • 30 | 31 |
  • 32 |
    33 |

    Furigana

    34 |
    35 |
    36 |

    次のステーション

    37 |
    38 |
    39 |
    40 |
    41 |
    42 | 43 | 51 | 52 | 53 | 61 | 62 | 63 | 71 |
    72 |
  • 73 | 74 |
  • 75 |

    Writing style

    76 |
    77 | 78 | 88 | 89 | 99 | 100 | 110 |
    111 |
  • 112 |
  • 113 |

    Output format

    114 | 123 |
  • 124 | 125 |
  • 126 | 127 |
  • 128 |
129 |
130 |
131 |
132 |
133 |

134 | This webapp annotates Japanese ebooks (EPUB, AZW3 or MOBI format) with furigana, 135 | to help Japanese language learners in their studies. 136 |

137 |

138 | For suggestions and reporting issues, click here. 139 |

140 |

141 | Source code available on project page 142 |

143 |
144 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.4.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 11 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 12 | ] 13 | 14 | [package.dependencies] 15 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 19 | 20 | [package.extras] 21 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 22 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 23 | trio = ["trio (>=0.23)"] 24 | 25 | [[package]] 26 | name = "cached-property" 27 | version = "1.5.2" 28 | description = "A decorator for caching properties in classes." 29 | optional = false 30 | python-versions = "*" 31 | files = [ 32 | {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, 33 | {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, 34 | ] 35 | 36 | [[package]] 37 | name = "capybre" 38 | version = "0.0.6" 39 | description = "Python interface for Calibre's command line tools" 40 | optional = false 41 | python-versions = ">=3.6" 42 | files = [ 43 | {file = "capybre-0.0.6-py3-none-any.whl", hash = "sha256:b10f6d8b979ac4dc9b6e75ffd331c2f88295e9bf13a9cc530ab21d2269993991"}, 44 | {file = "capybre-0.0.6.tar.gz", hash = "sha256:6db4c83b363b0546499306f194b2de60d88d13842c876d1d97050dea97802b85"}, 45 | ] 46 | 47 | [[package]] 48 | name = "chevron" 49 | version = "0.14.0" 50 | description = "Mustache templating language renderer" 51 | optional = false 52 | python-versions = "*" 53 | files = [ 54 | {file = "chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443"}, 55 | {file = "chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf"}, 56 | ] 57 | 58 | [[package]] 59 | name = "click" 60 | version = "7.1.2" 61 | description = "Composable command line interface toolkit" 62 | optional = false 63 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 64 | files = [ 65 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 66 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 67 | ] 68 | 69 | [[package]] 70 | name = "colorama" 71 | version = "0.4.6" 72 | description = "Cross-platform colored terminal text." 73 | optional = false 74 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 75 | files = [ 76 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 77 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 78 | ] 79 | 80 | [[package]] 81 | name = "exceptiongroup" 82 | version = "1.2.2" 83 | description = "Backport of PEP 654 (exception groups)" 84 | optional = false 85 | python-versions = ">=3.7" 86 | files = [ 87 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 88 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 89 | ] 90 | 91 | [package.extras] 92 | test = ["pytest (>=6)"] 93 | 94 | [[package]] 95 | name = "fastapi" 96 | version = "0.79.1" 97 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 98 | optional = false 99 | python-versions = ">=3.6.1" 100 | files = [ 101 | {file = "fastapi-0.79.1-py3-none-any.whl", hash = "sha256:3c584179c64e265749e88221c860520fc512ea37e253282dab378cc503dfd7fd"}, 102 | {file = "fastapi-0.79.1.tar.gz", hash = "sha256:006862dec0f0f5683ac21fb0864af2ff12a931e7ba18920f28cc8eceed51896b"}, 103 | ] 104 | 105 | [package.dependencies] 106 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 107 | starlette = "0.19.1" 108 | 109 | [package.extras] 110 | all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] 111 | dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] 112 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] 113 | test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] 114 | 115 | [[package]] 116 | name = "frozendict" 117 | version = "2.4.4" 118 | description = "A simple immutable dictionary" 119 | optional = false 120 | python-versions = ">=3.6" 121 | files = [ 122 | {file = "frozendict-2.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a59578d47b3949437519b5c39a016a6116b9e787bb19289e333faae81462e59"}, 123 | {file = "frozendict-2.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a342e439aef28ccec533f0253ea53d75fe9102bd6ea928ff530e76eac38906"}, 124 | {file = "frozendict-2.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f79c26dff10ce11dad3b3627c89bb2e87b9dd5958c2b24325f16a23019b8b94"}, 125 | {file = "frozendict-2.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2bd009cf4fc47972838a91e9b83654dc9a095dc4f2bb3a37c3f3124c8a364543"}, 126 | {file = "frozendict-2.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:87ebcde21565a14fe039672c25550060d6f6d88cf1f339beac094c3b10004eb0"}, 127 | {file = "frozendict-2.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:fefeb700bc7eb8b4c2dc48704e4221860d254c8989fb53488540bc44e44a1ac2"}, 128 | {file = "frozendict-2.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:4297d694eb600efa429769125a6f910ec02b85606f22f178bafbee309e7d3ec7"}, 129 | {file = "frozendict-2.4.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:812ab17522ba13637826e65454115a914c2da538356e85f43ecea069813e4b33"}, 130 | {file = "frozendict-2.4.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fee9420475bb6ff357000092aa9990c2f6182b2bab15764330f4ad7de2eae49"}, 131 | {file = "frozendict-2.4.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3148062675536724502c6344d7c485dd4667fdf7980ca9bd05e338ccc0c4471e"}, 132 | {file = "frozendict-2.4.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:78c94991944dd33c5376f720228e5b252ee67faf3bac50ef381adc9e51e90d9d"}, 133 | {file = "frozendict-2.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:1697793b5f62b416c0fc1d94638ec91ed3aa4ab277f6affa3a95216ecb3af170"}, 134 | {file = "frozendict-2.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:199a4d32194f3afed6258de7e317054155bc9519252b568d9cfffde7e4d834e5"}, 135 | {file = "frozendict-2.4.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85375ec6e979e6373bffb4f54576a68bf7497c350861d20686ccae38aab69c0a"}, 136 | {file = "frozendict-2.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2d8536e068d6bf281f23fa835ac07747fb0f8851879dd189e9709f9567408b4d"}, 137 | {file = "frozendict-2.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:259528ba6b56fa051bc996f1c4d8b57e30d6dd3bc2f27441891b04babc4b5e73"}, 138 | {file = "frozendict-2.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:07c3a5dee8bbb84cba770e273cdbf2c87c8e035903af8f781292d72583416801"}, 139 | {file = "frozendict-2.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6874fec816b37b6eb5795b00e0574cba261bf59723e2de607a195d5edaff0786"}, 140 | {file = "frozendict-2.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8f92425686323a950337da4b75b4c17a3327b831df8c881df24038d560640d4"}, 141 | {file = "frozendict-2.4.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d58d9a8d9e49662c6dafbea5e641f97decdb3d6ccd76e55e79818415362ba25"}, 142 | {file = "frozendict-2.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:93a7b19afb429cbf99d56faf436b45ef2fa8fe9aca89c49eb1610c3bd85f1760"}, 143 | {file = "frozendict-2.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b70b431e3a72d410a2cdf1497b3aba2f553635e0c0f657ce311d841bf8273b6"}, 144 | {file = "frozendict-2.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:e1b941132d79ce72d562a13341d38fc217bc1ee24d8c35a20d754e79ff99e038"}, 145 | {file = "frozendict-2.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc2228874eacae390e63fd4f2bb513b3144066a977dc192163c9f6c7f6de6474"}, 146 | {file = "frozendict-2.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63aa49f1919af7d45fb8fd5dec4c0859bc09f46880bd6297c79bb2db2969b63d"}, 147 | {file = "frozendict-2.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6bf9260018d653f3cab9bd147bd8592bf98a5c6e338be0491ced3c196c034a3"}, 148 | {file = "frozendict-2.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6eb716e6a6d693c03b1d53280a1947716129f5ef9bcdd061db5c17dea44b80fe"}, 149 | {file = "frozendict-2.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d13b4310db337f4d2103867c5a05090b22bc4d50ca842093779ef541ea9c9eea"}, 150 | {file = "frozendict-2.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:b3b967d5065872e27b06f785a80c0ed0a45d1f7c9b85223da05358e734d858ca"}, 151 | {file = "frozendict-2.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:4ae8d05c8d0b6134bfb6bfb369d5fa0c4df21eabb5ca7f645af95fdc6689678e"}, 152 | {file = "frozendict-2.4.4-py311-none-any.whl", hash = "sha256:705efca8d74d3facbb6ace80ab3afdd28eb8a237bfb4063ed89996b024bc443d"}, 153 | {file = "frozendict-2.4.4-py312-none-any.whl", hash = "sha256:d9647563e76adb05b7cde2172403123380871360a114f546b4ae1704510801e5"}, 154 | {file = "frozendict-2.4.4.tar.gz", hash = "sha256:3f7c031b26e4ee6a3f786ceb5e3abf1181c4ade92dce1f847da26ea2c96008c7"}, 155 | ] 156 | 157 | [[package]] 158 | name = "furigana" 159 | version = "0.0.8" 160 | description = "Convert Japanese Kanji to Kanji attached with Hiragana. For example, 「澱んだ街角」→「澱(よど)んだ街角(まちかど)」" 161 | optional = false 162 | python-versions = "*" 163 | files = [] 164 | develop = false 165 | 166 | [package.dependencies] 167 | jaconv = "0.3" 168 | mecab-python3 = ">=1.0.5" 169 | 170 | [package.source] 171 | type = "git" 172 | url = "https://github.com/itsupera/furigana.git" 173 | reference = "0.4" 174 | resolved_reference = "04f0be41258797c7eaaf73d4ec49650dbe93ee29" 175 | 176 | [[package]] 177 | name = "genanki" 178 | version = "0.13.0" 179 | description = "Generate Anki decks programmatically" 180 | optional = false 181 | python-versions = ">=3.6" 182 | files = [] 183 | develop = false 184 | 185 | [package.dependencies] 186 | cached-property = "*" 187 | chevron = "*" 188 | frozendict = "*" 189 | pyyaml = "*" 190 | 191 | [package.source] 192 | type = "git" 193 | url = "https://github.com/kerrickstaley/genanki" 194 | reference = "v0.13.0" 195 | resolved_reference = "5026448cb661570b2355afc5a45c1c9fcc9eea24" 196 | 197 | [[package]] 198 | name = "h11" 199 | version = "0.14.0" 200 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 201 | optional = false 202 | python-versions = ">=3.7" 203 | files = [ 204 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 205 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 206 | ] 207 | 208 | [[package]] 209 | name = "idna" 210 | version = "3.7" 211 | description = "Internationalized Domain Names in Applications (IDNA)" 212 | optional = false 213 | python-versions = ">=3.5" 214 | files = [ 215 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 216 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 217 | ] 218 | 219 | [[package]] 220 | name = "iniconfig" 221 | version = "2.0.0" 222 | description = "brain-dead simple config-ini parsing" 223 | optional = false 224 | python-versions = ">=3.7" 225 | files = [ 226 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 227 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 228 | ] 229 | 230 | [[package]] 231 | name = "jaconv" 232 | version = "0.3" 233 | description = "Pure-Python Japanese character interconverter for Hiragana, Katakana, Hankaku, Zenkaku and more" 234 | optional = false 235 | python-versions = "*" 236 | files = [ 237 | {file = "jaconv-0.3.tar.gz", hash = "sha256:cc70c796c19a6765598c03eac59d1399a555a9a8839cc70e540ec26f0ec3e66e"}, 238 | ] 239 | 240 | [[package]] 241 | name = "jinja2" 242 | version = "3.1.4" 243 | description = "A very fast and expressive template engine." 244 | optional = false 245 | python-versions = ">=3.7" 246 | files = [ 247 | {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, 248 | {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, 249 | ] 250 | 251 | [package.dependencies] 252 | MarkupSafe = ">=2.0" 253 | 254 | [package.extras] 255 | i18n = ["Babel (>=2.7)"] 256 | 257 | [[package]] 258 | name = "markupsafe" 259 | version = "2.1.5" 260 | description = "Safely add untrusted strings to HTML/XML markup." 261 | optional = false 262 | python-versions = ">=3.7" 263 | files = [ 264 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 265 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 266 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 267 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 268 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 269 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 270 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 271 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 272 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 273 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 274 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 275 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 276 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 277 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 278 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 279 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 280 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 281 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 282 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 283 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 284 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 285 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 286 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 287 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 288 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 289 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 290 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 291 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 292 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 293 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 294 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 295 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 296 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 297 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 298 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 299 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 300 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 301 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 302 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 303 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 304 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 305 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 306 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 307 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 308 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 309 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 310 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 311 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 312 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 313 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 314 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 315 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 316 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 317 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 318 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 319 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 320 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 321 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 322 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 323 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 324 | ] 325 | 326 | [[package]] 327 | name = "mecab-python3" 328 | version = "1.0.9" 329 | description = "Python wrapper for the MeCab morphological analyzer for Japanese" 330 | optional = false 331 | python-versions = "*" 332 | files = [ 333 | {file = "mecab_python3-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f335f924b2ddd495b7e8e8e194014085a3231fe470d1d4da752b463feef0986c"}, 334 | {file = "mecab_python3-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3a28e443fb85e6b5d39ff7f2b6885f7b42b7e88f8a646640656d08048bf6daf3"}, 335 | {file = "mecab_python3-1.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb0289922db4c3fa8a0e07a3cb00842e3446ae8f1a81b2527774d33289be1756"}, 336 | {file = "mecab_python3-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8047db1a2abf8b5cc251a92e0758c26543625013cc9150ab6d8ca66b4de789a1"}, 337 | {file = "mecab_python3-1.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52697413a98f9614df3811166c2c9780e9d4254c8038f9a9a496580f98424f4c"}, 338 | {file = "mecab_python3-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:a75b8e5f1e3041ae924b2420716dd797e73b96d573fc660f140bd506ed6d61cf"}, 339 | {file = "mecab_python3-1.0.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:920fd996217cd0946fcc38448c11a215780285cce7d248008549fb64780c8445"}, 340 | {file = "mecab_python3-1.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2220967c7c1027ff6d71cee1a08b2917f42e0a179f43069a6cc1e362d825630a"}, 341 | {file = "mecab_python3-1.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfb874e08e2277317334459dbe89444c72d75b29e0cd29d5f8b0c200b10684c0"}, 342 | {file = "mecab_python3-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:346b553e06ac619a5013c750baf11f81b967c391734c7ddef250ba80986eff1d"}, 343 | {file = "mecab_python3-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad8814e7527b975a0152106b3199c3008b3e02009530e8ddc82a3aec736ec05"}, 344 | {file = "mecab_python3-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:c861ed23ec0e0fb0f295ca1119067a9b8f221f50621d720b0c030977a00ba0da"}, 345 | {file = "mecab_python3-1.0.9-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a29211f50109849ae4d8b1901014fa1f0e792b86d3b45921f1971ea15c0b7c02"}, 346 | {file = "mecab_python3-1.0.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cd494bef7377d7b1789778d82c861eed9a6ee08de9b0d7d38641284dfc584015"}, 347 | {file = "mecab_python3-1.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09c5a2c08533f691fdad6ead373cea860b85acd5dede770f9627654712d47fa0"}, 348 | {file = "mecab_python3-1.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5a7936271d44c4ec6231ddb5fed53a404302ec48201b784c85f19989452c0f2"}, 349 | {file = "mecab_python3-1.0.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c79542fd92ac4ecdd9740077df6ddb095fa322a13f660f83ef43f43fbe0877"}, 350 | {file = "mecab_python3-1.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:b3ef376016cdef012c62b92ea30c3aadcb12349c3afac8df7ade2eafe3850fd2"}, 351 | {file = "mecab_python3-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c2429796c84a81608f120b7443ae531e5ea416234a077d5111f15f09569b714"}, 352 | {file = "mecab_python3-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:200c76d17a270eadd7e39ddeff243495cb24d514cadda01d8a69903a9d82f303"}, 353 | {file = "mecab_python3-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d125b7bc9f8cfa1a03d20d84c4118c0166288cedac9abf9b21a910c65d250aef"}, 354 | {file = "mecab_python3-1.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a367595535562bcd25a2643e7a20bc058ab28ee9e2e186c576a005f1578f1a95"}, 355 | {file = "mecab_python3-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:784f178beffc35a38c44074b020ae87ecc7f2dc8e9d17450e399aee36a965b5b"}, 356 | {file = "mecab_python3-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdefc23384b439abd104370694d44feb6f9fbc6587e47797e7d240e12e293fa6"}, 357 | {file = "mecab_python3-1.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1de6ba0012538f016232be1f1853cec314c97a38667fbba795151d9644d0427f"}, 358 | {file = "mecab_python3-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3574d6c8aca17c16f3f12c28eda6b151263688a886833f8ef83391b85ec8d95f"}, 359 | {file = "mecab_python3-1.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4676475b7fe53431c0cfb0c1953a4e162cf3abfca912bcee358042645b28c0bd"}, 360 | {file = "mecab_python3-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:6580b8fb6f28073d0c09c709c33ca05dfbf791eb859f3541b6756037b5d57f36"}, 361 | {file = "mecab_python3-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7180579dba525cdf082bef413ccbbc53087cd799846794eee626d920a591eb25"}, 362 | {file = "mecab_python3-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93247a6a5ffcb91102a6a84b86bac264c309f2517370cfdbaaf9388975189639"}, 363 | {file = "mecab_python3-1.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a658c4ca3e36f61472a648d19559d98d3c47bbecba4e611b681afa0f7651a1c7"}, 364 | {file = "mecab_python3-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673e2cf3e31030ef2e2f7a96fc4f4c8bb5673336483a8546d69c0f19f34f0e78"}, 365 | {file = "mecab_python3-1.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b3cb42213af56c7d50e657fdd6757d48a06e2d851f1495172b9602602c70548"}, 366 | {file = "mecab_python3-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:25f510a04de7d65445e3872f55d37d5adf11c5e1da99ea1b788cd30f29e8d89f"}, 367 | {file = "mecab_python3-1.0.9.tar.gz", hash = "sha256:2d891f4a0119fb766fa796e28d881a55793a0c3544e446cff64dc946ad7227cc"}, 368 | ] 369 | 370 | [package.extras] 371 | unidic = ["unidic"] 372 | unidic-lite = ["unidic-lite"] 373 | 374 | [[package]] 375 | name = "packaging" 376 | version = "24.1" 377 | description = "Core utilities for Python packages" 378 | optional = false 379 | python-versions = ">=3.8" 380 | files = [ 381 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 382 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 383 | ] 384 | 385 | [[package]] 386 | name = "pluggy" 387 | version = "1.5.0" 388 | description = "plugin and hook calling mechanisms for python" 389 | optional = false 390 | python-versions = ">=3.8" 391 | files = [ 392 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 393 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 394 | ] 395 | 396 | [package.extras] 397 | dev = ["pre-commit", "tox"] 398 | testing = ["pytest", "pytest-benchmark"] 399 | 400 | [[package]] 401 | name = "pydantic" 402 | version = "1.10.17" 403 | description = "Data validation and settings management using python type hints" 404 | optional = false 405 | python-versions = ">=3.7" 406 | files = [ 407 | {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, 408 | {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, 409 | {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, 410 | {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, 411 | {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, 412 | {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, 413 | {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, 414 | {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, 415 | {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, 416 | {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, 417 | {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, 418 | {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, 419 | {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, 420 | {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, 421 | {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, 422 | {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, 423 | {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, 424 | {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, 425 | {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, 426 | {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, 427 | {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, 428 | {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, 429 | {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, 430 | {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, 431 | {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, 432 | {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, 433 | {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, 434 | {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, 435 | {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, 436 | {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, 437 | {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, 438 | {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, 439 | {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, 440 | {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, 441 | {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, 442 | {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, 443 | {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, 444 | {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, 445 | {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, 446 | {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, 447 | {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, 448 | {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, 449 | {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, 450 | ] 451 | 452 | [package.dependencies] 453 | typing-extensions = ">=4.2.0" 454 | 455 | [package.extras] 456 | dotenv = ["python-dotenv (>=0.10.4)"] 457 | email = ["email-validator (>=1.0.3)"] 458 | 459 | [[package]] 460 | name = "pypandoc" 461 | version = "1.13" 462 | description = "Thin wrapper for pandoc." 463 | optional = false 464 | python-versions = ">=3.6" 465 | files = [ 466 | {file = "pypandoc-1.13-py3-none-any.whl", hash = "sha256:4c7d71bf2f1ed122aac287113b5c4d537a33bbc3c1df5aed11a7d4a7ac074681"}, 467 | {file = "pypandoc-1.13.tar.gz", hash = "sha256:31652073c7960c2b03570bd1e94f602ca9bc3e70099df5ead4cea98ff5151c1e"}, 468 | ] 469 | 470 | [[package]] 471 | name = "pytest" 472 | version = "8.2.2" 473 | description = "pytest: simple powerful testing with Python" 474 | optional = false 475 | python-versions = ">=3.8" 476 | files = [ 477 | {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, 478 | {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, 479 | ] 480 | 481 | [package.dependencies] 482 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 483 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 484 | iniconfig = "*" 485 | packaging = "*" 486 | pluggy = ">=1.5,<2.0" 487 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 488 | 489 | [package.extras] 490 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 491 | 492 | [[package]] 493 | name = "python-multipart" 494 | version = "0.0.5" 495 | description = "A streaming multipart parser for Python" 496 | optional = false 497 | python-versions = "*" 498 | files = [ 499 | {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, 500 | ] 501 | 502 | [package.dependencies] 503 | six = ">=1.4.0" 504 | 505 | [[package]] 506 | name = "pyyaml" 507 | version = "6.0.1" 508 | description = "YAML parser and emitter for Python" 509 | optional = false 510 | python-versions = ">=3.6" 511 | files = [ 512 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 513 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 514 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 515 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 516 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 517 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 518 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 519 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 520 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 521 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 522 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 523 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 524 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 525 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 526 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 527 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 528 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 529 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 530 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 531 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 532 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 533 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 534 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 535 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 536 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 537 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 538 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 539 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 540 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 541 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 542 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 543 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 544 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 545 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 546 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 547 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 548 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 549 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 550 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 551 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 552 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 553 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 554 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 555 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 556 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 557 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 558 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 559 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 560 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 561 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 562 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 563 | ] 564 | 565 | [[package]] 566 | name = "ruff" 567 | version = "0.5.7" 568 | description = "An extremely fast Python linter and code formatter, written in Rust." 569 | optional = false 570 | python-versions = ">=3.7" 571 | files = [ 572 | {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, 573 | {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, 574 | {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, 575 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, 576 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, 577 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, 578 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, 579 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, 580 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, 581 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, 582 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, 583 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, 584 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, 585 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, 586 | {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, 587 | {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, 588 | {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, 589 | {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, 590 | ] 591 | 592 | [[package]] 593 | name = "six" 594 | version = "1.16.0" 595 | description = "Python 2 and 3 compatibility utilities" 596 | optional = false 597 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 598 | files = [ 599 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 600 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 601 | ] 602 | 603 | [[package]] 604 | name = "sniffio" 605 | version = "1.3.1" 606 | description = "Sniff out which async library your code is running under" 607 | optional = false 608 | python-versions = ">=3.7" 609 | files = [ 610 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 611 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 612 | ] 613 | 614 | [[package]] 615 | name = "starlette" 616 | version = "0.19.1" 617 | description = "The little ASGI library that shines." 618 | optional = false 619 | python-versions = ">=3.6" 620 | files = [ 621 | {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, 622 | {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, 623 | ] 624 | 625 | [package.dependencies] 626 | anyio = ">=3.4.0,<5" 627 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 628 | 629 | [package.extras] 630 | full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] 631 | 632 | [[package]] 633 | name = "tomli" 634 | version = "2.0.1" 635 | description = "A lil' TOML parser" 636 | optional = false 637 | python-versions = ">=3.7" 638 | files = [ 639 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 640 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 641 | ] 642 | 643 | [[package]] 644 | name = "typer" 645 | version = "0.3.2" 646 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 647 | optional = false 648 | python-versions = ">=3.6" 649 | files = [ 650 | {file = "typer-0.3.2-py3-none-any.whl", hash = "sha256:ba58b920ce851b12a2d790143009fa00ac1d05b3ff3257061ff69dbdfc3d161b"}, 651 | {file = "typer-0.3.2.tar.gz", hash = "sha256:5455d750122cff96745b0dec87368f56d023725a7ebc9d2e54dd23dc86816303"}, 652 | ] 653 | 654 | [package.dependencies] 655 | click = ">=7.1.1,<7.2.0" 656 | 657 | [package.extras] 658 | all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] 659 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] 660 | doc = ["markdown-include (>=0.5.1,<0.6.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)"] 661 | test = ["black (>=19.10b0,<20.0b0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.782)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 662 | 663 | [[package]] 664 | name = "typing-extensions" 665 | version = "4.12.2" 666 | description = "Backported and Experimental Type Hints for Python 3.8+" 667 | optional = false 668 | python-versions = ">=3.8" 669 | files = [ 670 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 671 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 672 | ] 673 | 674 | [[package]] 675 | name = "uvicorn" 676 | version = "0.18.3" 677 | description = "The lightning-fast ASGI server." 678 | optional = false 679 | python-versions = ">=3.7" 680 | files = [ 681 | {file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"}, 682 | {file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"}, 683 | ] 684 | 685 | [package.dependencies] 686 | click = ">=7.0" 687 | h11 = ">=0.8" 688 | 689 | [package.extras] 690 | standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] 691 | 692 | [metadata] 693 | lock-version = "2.0" 694 | python-versions = "^3.8" 695 | content-hash = "7e29b30a4a559337a79f857b2e6350c4a01302bb1a34c052ffdf87073a9cfe3b" 696 | --------------------------------------------------------------------------------