mirror of
https://github.com/opendatalab/MinerU.git
synced 2026-09-19 09:30:53 +08:00
feat: mineru_web
This commit is contained in:
@@ -3,3 +3,5 @@
|
||||
## 项目列表
|
||||
|
||||
- [llama_index_rag](./llama_index_rag/README.md): 基于 llama_index 构建轻量级 RAG 系统
|
||||
|
||||
- [web_api](./web_api/README.md): PDF解析的restful api服务
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
## 安装
|
||||
|
||||
MinerU
|
||||
|
||||
```bash
|
||||
# mineru已安装则跳过此步骤
|
||||
|
||||
git clone https://github.com/opendatalab/MinerU.git
|
||||
cd MinerU
|
||||
|
||||
conda create -n MinerU python=3.10
|
||||
conda activate MinerU
|
||||
pip install .[full] --extra-index-url https://wheels.myhloli.com
|
||||
```
|
||||
|
||||
第三方软件
|
||||
|
||||
```bash
|
||||
cd projects/web_api
|
||||
pip install poetry
|
||||
portey install
|
||||
```
|
||||
|
||||
接口文档
|
||||
```
|
||||
在浏览器打开 mineru-web接口文档.html
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
Generated
+687
@@ -0,0 +1,687 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.13.2"
|
||||
description = "A database migration tool for SQLAlchemy."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"},
|
||||
{file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Mako = "*"
|
||||
SQLAlchemy = ">=1.3.0"
|
||||
typing-extensions = ">=4"
|
||||
|
||||
[package.extras]
|
||||
tz = ["backports.zoneinfo"]
|
||||
|
||||
[[package]]
|
||||
name = "aniso8601"
|
||||
version = "9.0.1"
|
||||
description = "A library for parsing ISO 8601 strings."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"},
|
||||
{file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.8.2"
|
||||
description = "Fast, simple object-to-object and broadcast signaling"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"},
|
||||
{file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.0.3"
|
||||
description = "A simple framework for building complex web applications."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"},
|
||||
{file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
blinker = ">=1.6.2"
|
||||
click = ">=8.1.3"
|
||||
itsdangerous = ">=2.1.2"
|
||||
Jinja2 = ">=3.1.2"
|
||||
Werkzeug = ">=3.0.0"
|
||||
|
||||
[package.extras]
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-cors"
|
||||
version = "5.0.0"
|
||||
description = "A Flask extension adding a decorator for CORS support"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"},
|
||||
{file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=0.9"
|
||||
|
||||
[[package]]
|
||||
name = "flask-jwt-extended"
|
||||
version = "4.6.0"
|
||||
description = "Extended JWT integration with Flask"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4"
|
||||
files = [
|
||||
{file = "Flask-JWT-Extended-4.6.0.tar.gz", hash = "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2"},
|
||||
{file = "Flask_JWT_Extended-4.6.0-py2.py3-none-any.whl", hash = "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=2.0,<4.0"
|
||||
PyJWT = ">=2.0,<3.0"
|
||||
Werkzeug = ">=0.14"
|
||||
|
||||
[package.extras]
|
||||
asymmetric-crypto = ["cryptography (>=3.3.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-marshmallow"
|
||||
version = "1.2.1"
|
||||
description = "Flask + marshmallow for beautiful APIs"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "flask_marshmallow-1.2.1-py3-none-any.whl", hash = "sha256:10b5048ecfaa26f7c8d0aed7d81083164450e6be8e81c04b3d4a586b3f7b6678"},
|
||||
{file = "flask_marshmallow-1.2.1.tar.gz", hash = "sha256:00ee96399ed664963afff3b5d6ee518640b0f91dbc2aace2b5abcf32f40ef23a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=2.2"
|
||||
marshmallow = ">=3.0.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["flask-marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
|
||||
docs = ["Sphinx (==7.2.6)", "marshmallow-sqlalchemy (>=0.19.0)", "sphinx-issues (==4.0.0)"]
|
||||
sqlalchemy = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.29.0)"]
|
||||
tests = ["flask-marshmallow[sqlalchemy]", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-migrate"
|
||||
version = "4.0.7"
|
||||
description = "SQLAlchemy database migrations for Flask applications using Alembic."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "Flask-Migrate-4.0.7.tar.gz", hash = "sha256:dff7dd25113c210b069af280ea713b883f3840c1e3455274745d7355778c8622"},
|
||||
{file = "Flask_Migrate-4.0.7-py3-none-any.whl", hash = "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alembic = ">=1.9.0"
|
||||
Flask = ">=0.9"
|
||||
Flask-SQLAlchemy = ">=1.0"
|
||||
|
||||
[[package]]
|
||||
name = "flask-restful"
|
||||
version = "0.3.10"
|
||||
description = "Simple framework for creating REST APIs"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Flask-RESTful-0.3.10.tar.gz", hash = "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37"},
|
||||
{file = "Flask_RESTful-0.3.10-py2.py3-none-any.whl", hash = "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aniso8601 = ">=0.82"
|
||||
Flask = ">=0.8"
|
||||
pytz = "*"
|
||||
six = ">=1.3.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-sqlalchemy"
|
||||
version = "3.1.1"
|
||||
description = "Add SQLAlchemy support to your Flask application."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0"},
|
||||
{file = "flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
flask = ">=2.2.5"
|
||||
sqlalchemy = ">=2.0.16"
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.0.3"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"},
|
||||
{file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"},
|
||||
{file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"},
|
||||
{file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"},
|
||||
{file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"},
|
||||
{file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"},
|
||||
{file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"},
|
||||
{file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "furo"]
|
||||
test = ["objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
description = "Safely pass data to untrusted environments and back."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
|
||||
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.4"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.2"
|
||||
description = "Python logging made (stupidly) simple"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"},
|
||||
{file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
|
||||
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.5"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"},
|
||||
{file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.9.2"
|
||||
|
||||
[package.extras]
|
||||
babel = ["Babel"]
|
||||
lingua = ["lingua"]
|
||||
testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.5"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
|
||||
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.22.0"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9"},
|
||||
{file = "marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=17.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
|
||||
docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow-sqlalchemy"
|
||||
version = "1.1.0"
|
||||
description = "SQLAlchemy integration with the marshmallow (de)serialization library"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "marshmallow_sqlalchemy-1.1.0-py3-none-any.whl", hash = "sha256:cce261148e4c6ec4ee275f3d29352933380a1afa2fd3933f5e9ecd02fdc16ade"},
|
||||
{file = "marshmallow_sqlalchemy-1.1.0.tar.gz", hash = "sha256:2ab092da269dafa8a05d51a58409af71a8d2183958ba47143127dd239e0359d8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
marshmallow = ">=3.18.0"
|
||||
SQLAlchemy = ">=1.4.40,<3.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["marshmallow-sqlalchemy[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
|
||||
docs = ["alabaster (==1.0.0)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)"]
|
||||
tests = ["pytest (<9)", "pytest-lazy-fixtures"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.9.0"
|
||||
description = "JSON Web Token implementation in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
|
||||
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
crypto = ["cryptography (>=3.4.0)"]
|
||||
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2024.1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
|
||||
{file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
|
||||
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
|
||||
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
|
||||
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
|
||||
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
|
||||
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
|
||||
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
|
||||
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
|
||||
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
|
||||
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
|
||||
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
|
||||
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
|
||||
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.32"
|
||||
description = "Database Abstraction Library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c9045ecc2e4db59bfc97b20516dfdf8e41d910ac6fb667ebd3a79ea54084619"},
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1467940318e4a860afd546ef61fefb98a14d935cd6817ed07a228c7f7c62f389"},
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5954463675cb15db8d4b521f3566a017c8789222b8316b1e6934c811018ee08b"},
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167e7497035c303ae50651b351c28dc22a40bb98fbdb8468cdc971821b1ae533"},
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b27dfb676ac02529fb6e343b3a482303f16e6bc3a4d868b73935b8792edb52d0"},
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bf2360a5e0f7bd75fa80431bf8ebcfb920c9f885e7956c7efde89031695cafb8"},
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-win32.whl", hash = "sha256:306fe44e754a91cd9d600a6b070c1f2fadbb4a1a257b8781ccf33c7067fd3e4d"},
|
||||
{file = "SQLAlchemy-2.0.32-cp310-cp310-win_amd64.whl", hash = "sha256:99db65e6f3ab42e06c318f15c98f59a436f1c78179e6a6f40f529c8cc7100b22"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21b053be28a8a414f2ddd401f1be8361e41032d2ef5884b2f31d31cb723e559f"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b178e875a7a25b5938b53b006598ee7645172fccafe1c291a706e93f48499ff5"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723a40ee2cc7ea653645bd4cf024326dea2076673fc9d3d33f20f6c81db83e1d"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:295ff8689544f7ee7e819529633d058bd458c1fd7f7e3eebd0f9268ebc56c2a0"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49496b68cd190a147118af585173ee624114dfb2e0297558c460ad7495f9dfe2"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:acd9b73c5c15f0ec5ce18128b1fe9157ddd0044abc373e6ecd5ba376a7e5d961"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-win32.whl", hash = "sha256:9365a3da32dabd3e69e06b972b1ffb0c89668994c7e8e75ce21d3e5e69ddef28"},
|
||||
{file = "SQLAlchemy-2.0.32-cp311-cp311-win_amd64.whl", hash = "sha256:8bd63d051f4f313b102a2af1cbc8b80f061bf78f3d5bd0843ff70b5859e27924"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bab3db192a0c35e3c9d1560eb8332463e29e5507dbd822e29a0a3c48c0a8d92"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:19d98f4f58b13900d8dec4ed09dd09ef292208ee44cc9c2fe01c1f0a2fe440e9"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd33c61513cb1b7371fd40cf221256456d26a56284e7d19d1f0b9f1eb7dd7e8"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6ba0497c1d066dd004e0f02a92426ca2df20fac08728d03f67f6960271feec"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b6be53e4fde0065524f1a0a7929b10e9280987b320716c1509478b712a7688c"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:916a798f62f410c0b80b63683c8061f5ebe237b0f4ad778739304253353bc1cb"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-win32.whl", hash = "sha256:31983018b74908ebc6c996a16ad3690301a23befb643093fcfe85efd292e384d"},
|
||||
{file = "SQLAlchemy-2.0.32-cp312-cp312-win_amd64.whl", hash = "sha256:4363ed245a6231f2e2957cccdda3c776265a75851f4753c60f3004b90e69bfeb"},
|
||||
{file = "SQLAlchemy-2.0.32-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8afd5b26570bf41c35c0121801479958b4446751a3971fb9a480c1afd85558e"},
|
||||
{file = "SQLAlchemy-2.0.32-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c750987fc876813f27b60d619b987b057eb4896b81117f73bb8d9918c14f1cad"},
|
||||
{file = "SQLAlchemy-2.0.32-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada0102afff4890f651ed91120c1120065663506b760da4e7823913ebd3258be"},
|
||||
{file = "SQLAlchemy-2.0.32-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:78c03d0f8a5ab4f3034c0e8482cfcc415a3ec6193491cfa1c643ed707d476f16"},
|
||||
{file = "SQLAlchemy-2.0.32-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:3bd1cae7519283ff525e64645ebd7a3e0283f3c038f461ecc1c7b040a0c932a1"},
|
||||
{file = "SQLAlchemy-2.0.32-cp37-cp37m-win32.whl", hash = "sha256:01438ebcdc566d58c93af0171c74ec28efe6a29184b773e378a385e6215389da"},
|
||||
{file = "SQLAlchemy-2.0.32-cp37-cp37m-win_amd64.whl", hash = "sha256:4979dc80fbbc9d2ef569e71e0896990bc94df2b9fdbd878290bd129b65ab579c"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c742be912f57586ac43af38b3848f7688863a403dfb220193a882ea60e1ec3a"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62e23d0ac103bcf1c5555b6c88c114089587bc64d048fef5bbdb58dfd26f96da"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:251f0d1108aab8ea7b9aadbd07fb47fb8e3a5838dde34aa95a3349876b5a1f1d"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef18a84e5116340e38eca3e7f9eeaaef62738891422e7c2a0b80feab165905f"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3eb6a97a1d39976f360b10ff208c73afb6a4de86dd2a6212ddf65c4a6a2347d5"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0c1c9b673d21477cec17ab10bc4decb1322843ba35b481585facd88203754fc5"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-win32.whl", hash = "sha256:c41a2b9ca80ee555decc605bd3c4520cc6fef9abde8fd66b1cf65126a6922d65"},
|
||||
{file = "SQLAlchemy-2.0.32-cp38-cp38-win_amd64.whl", hash = "sha256:8a37e4d265033c897892279e8adf505c8b6b4075f2b40d77afb31f7185cd6ecd"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fec964fba2ef46476312a03ec8c425956b05c20220a1a03703537824b5e8e1"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:328429aecaba2aee3d71e11f2477c14eec5990fb6d0e884107935f7fb6001632"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85a01b5599e790e76ac3fe3aa2f26e1feba56270023d6afd5550ed63c68552b3"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf04784797dcdf4c0aa952c8d234fa01974c4729db55c45732520ce12dd95b4"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4488120becf9b71b3ac718f4138269a6be99a42fe023ec457896ba4f80749525"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14e09e083a5796d513918a66f3d6aedbc131e39e80875afe81d98a03312889e6"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-win32.whl", hash = "sha256:0d322cc9c9b2154ba7e82f7bf25ecc7c36fbe2d82e2933b3642fc095a52cfc78"},
|
||||
{file = "SQLAlchemy-2.0.32-cp39-cp39-win_amd64.whl", hash = "sha256:7dd8583df2f98dea28b5cd53a1beac963f4f9d087888d75f22fcc93a07cf8d84"},
|
||||
{file = "SQLAlchemy-2.0.32-py3-none-any.whl", hash = "sha256:e567a8793a692451f706b363ccf3c45e056b67d90ead58c3bc9471af5d212202"},
|
||||
{file = "SQLAlchemy-2.0.32.tar.gz", hash = "sha256:c1b88cc8b02b6a5f0efb0345a03672d4c897dc7d92585176f88c67346f565ea8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
|
||||
aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)"]
|
||||
mysql = ["mysqlclient (>=1.4.0)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=8)"]
|
||||
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
|
||||
postgresql-psycopg = ["psycopg (>=3.0.7)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
|
||||
pymysql = ["pymysql"]
|
||||
sqlcipher = ["sqlcipher3_binary"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.0.4"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c"},
|
||||
{file = "werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.1.1"
|
||||
|
||||
[package.extras]
|
||||
watchdog = ["watchdog (>=2.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.1.0"
|
||||
description = "A small Python utility to set file creation time on Windows"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
|
||||
{file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "dda1a445d3e4ac500b0e776fae43a153624d8d62cb91ca0f2584837b622a42a7"
|
||||
@@ -0,0 +1,24 @@
|
||||
[tool.poetry]
|
||||
name = "web-api"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["houlinfeng <m15237195947@163.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
flask = "^3.0.3"
|
||||
flask-restful = "^0.3.10"
|
||||
flask-cors = "^5.0.0"
|
||||
flask-sqlalchemy = "^3.1.1"
|
||||
flask-migrate = "^4.0.7"
|
||||
flask-jwt-extended = "^4.6.0"
|
||||
flask-marshmallow = "^1.2.1"
|
||||
pyyaml = "^6.0.2"
|
||||
loguru = "^0.7.2"
|
||||
marshmallow-sqlalchemy = "^1.1.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -0,0 +1 @@
|
||||
__all__ = ["common", "api"]
|
||||
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
from .extentions import app, db, migrate, jwt, ma
|
||||
from common.web_hook import before_request
|
||||
from common.logger import setup_log
|
||||
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
print("root_dir", root_dir)
|
||||
|
||||
def _register_db(flask_app):
|
||||
from common import import_models
|
||||
db.init_app(flask_app)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
|
||||
def create_app(config):
|
||||
"""
|
||||
Create and configure an instance of the Flask application
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
app.static_folder = os.path.join(root_dir, "static")
|
||||
if config is None:
|
||||
config = {}
|
||||
app.config.update(config)
|
||||
setup_log(config)
|
||||
_register_db(app)
|
||||
migrate.init_app(app=app, db=db)
|
||||
jwt.init_app(app=app)
|
||||
ma.init_app(app=app)
|
||||
from .analysis import analysis_blue
|
||||
app.register_blueprint(analysis_blue)
|
||||
|
||||
app.before_request(before_request)
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,18 @@
|
||||
from flask import Blueprint
|
||||
from ..extentions import Api
|
||||
from .upload_view import UploadPdfView
|
||||
from .analysis_view import AnalysisTaskView, AnalysisTaskProgressView
|
||||
from .img_md_view import ImgView, MdView
|
||||
from .task_view import TaskView, HistoricalTasksView, DeleteTaskView
|
||||
|
||||
analysis_blue = Blueprint('analysis', __name__)
|
||||
|
||||
api_v2 = Api(analysis_blue, prefix='/api/v2')
|
||||
api_v2.add_resource(UploadPdfView, '/analysis/upload_pdf')
|
||||
api_v2.add_resource(AnalysisTaskView, '/extract/task/submit')
|
||||
api_v2.add_resource(AnalysisTaskProgressView, '/extract/task/progress')
|
||||
api_v2.add_resource(ImgView, '/analysis/pdf_img')
|
||||
api_v2.add_resource(MdView, '/analysis/pdf_md')
|
||||
api_v2.add_resource(TaskView, '/extract/taskQueue')
|
||||
api_v2.add_resource(HistoricalTasksView, '/extract/list')
|
||||
api_v2.add_resource(DeleteTaskView, '/extract/task')
|
||||
@@ -0,0 +1,231 @@
|
||||
import json
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from flask import request, current_app, url_for
|
||||
from flask_restful import Resource
|
||||
from .ext import find_file, task_state_map
|
||||
# from .formula_ext import formula_detection, formula_recognition
|
||||
from .serialization import AnalysisViewSchema
|
||||
from marshmallow import ValidationError
|
||||
from ..extentions import db
|
||||
from .models import AnalysisTask, AnalysisPdf
|
||||
from .pdf_ext import analysis_pdf_task
|
||||
from common.custom_response import generate_response
|
||||
|
||||
|
||||
class AnalysisTaskProgressView(Resource):
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
获取任务进度
|
||||
:return:
|
||||
"""
|
||||
params = request.args
|
||||
id = params.get('id')
|
||||
analysis_task = AnalysisTask.query.filter(AnalysisTask.id == id).first()
|
||||
if not analysis_task:
|
||||
return generate_response(code=400, msg="Invalid ID", msgZH="无效id")
|
||||
match analysis_task.task_type:
|
||||
case 'pdf':
|
||||
analysis_pdf = AnalysisPdf.query.filter(AnalysisPdf.id == analysis_task.analysis_pdf_id).first()
|
||||
file_url = url_for('analysis.uploadpdfview', filename=analysis_task.file_name, as_attachment=False)
|
||||
if analysis_task.status == 0:
|
||||
data = {
|
||||
"state": task_state_map.get(analysis_task.status),
|
||||
"status": analysis_pdf.status,
|
||||
"url": file_url,
|
||||
"fileName": analysis_task.file_name,
|
||||
"content": [],
|
||||
"markdownUrl": [],
|
||||
"fullMdLink": "",
|
||||
"type": analysis_task.task_type,
|
||||
}
|
||||
return generate_response(data=data)
|
||||
elif analysis_task.status == 1:
|
||||
if analysis_pdf.status == 1: # 任务正常完成
|
||||
bbox_info = json.loads(analysis_pdf.bbox_info)
|
||||
md_link_list = json.loads(analysis_pdf.md_link_list)
|
||||
full_md_link = analysis_pdf.full_md_link
|
||||
data = {
|
||||
"state": task_state_map.get(analysis_task.status),
|
||||
"status": analysis_pdf.status,
|
||||
"url": file_url,
|
||||
"fileName": analysis_task.file_name,
|
||||
"content": bbox_info,
|
||||
"markdownUrl": md_link_list,
|
||||
"fullMdLink": full_md_link,
|
||||
"type": analysis_task.task_type,
|
||||
}
|
||||
return generate_response(data=data)
|
||||
else: # 任务异常结束
|
||||
data = {
|
||||
"state": task_state_map.get(analysis_task.status),
|
||||
"status": analysis_pdf.status,
|
||||
"url": file_url,
|
||||
"fileName": analysis_task.file_name,
|
||||
"content": [],
|
||||
"markdownUrl": [],
|
||||
"fullMdLink": "",
|
||||
"type": analysis_task.task_type,
|
||||
}
|
||||
return generate_response(code=-60004, data=data, msg="Failed to retrieve PDF parsing progress",
|
||||
msgZh="无法获取PDF解析进度")
|
||||
else:
|
||||
data = {
|
||||
"state": task_state_map.get(analysis_task.status),
|
||||
"status": analysis_pdf.status,
|
||||
"url": file_url,
|
||||
"fileName": analysis_task.file_name,
|
||||
"content": [],
|
||||
"markdownUrl": [],
|
||||
"fullMdLink": "",
|
||||
"type": analysis_task.task_type,
|
||||
}
|
||||
return generate_response(data=data)
|
||||
case 'formula-detect':
|
||||
pass
|
||||
case 'formula-extract':
|
||||
pass
|
||||
case 'table-recogn':
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="尚不支持")
|
||||
case _:
|
||||
return generate_response()
|
||||
|
||||
|
||||
class AnalysisTaskView(Resource):
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
提交任务
|
||||
:return:
|
||||
"""
|
||||
analysis_view_schema = AnalysisViewSchema()
|
||||
try:
|
||||
params = analysis_view_schema.load(request.get_json())
|
||||
except ValidationError as err:
|
||||
return generate_response(code=400, msg=err.messages)
|
||||
file_key = params.get("fileKey")
|
||||
file_name = params.get("fileName")
|
||||
task_type = params.get("taskType")
|
||||
is_ocr = params.get("isOcr", False)
|
||||
|
||||
pdf_upload_folder = current_app.config['PDF_UPLOAD_FOLDER']
|
||||
upload_dir = f"{current_app.static_folder}/{pdf_upload_folder}"
|
||||
file_path = find_file(file_key, upload_dir)
|
||||
match task_type:
|
||||
case 'pdf':
|
||||
if not file_path:
|
||||
return generate_response(code=400, msg="FileKey is invalid, no PDF file found",
|
||||
msgZH="fileKey无效,未找到pdf文件")
|
||||
analysis_task = AnalysisTask.query.filter(AnalysisTask.status.in_([0, 2])).first()
|
||||
file_name = Path(file_path).name
|
||||
with db.auto_commit():
|
||||
analysis_pdf_object = AnalysisPdf(
|
||||
file_name=file_name,
|
||||
file_path=file_path,
|
||||
status=3 if analysis_task else 0,
|
||||
)
|
||||
db.session.add(analysis_pdf_object)
|
||||
db.session.flush()
|
||||
analysis_pdf_id = analysis_pdf_object.id
|
||||
with db.auto_commit():
|
||||
analysis_task_object = AnalysisTask(
|
||||
file_key=file_key,
|
||||
file_name=file_name,
|
||||
task_type=task_type,
|
||||
is_ocr=is_ocr,
|
||||
status=2 if analysis_task else 0,
|
||||
analysis_pdf_id=analysis_pdf_id
|
||||
)
|
||||
db.session.add(analysis_task_object)
|
||||
db.session.flush()
|
||||
analysis_task_id = analysis_task_object.id
|
||||
if not analysis_task: # 已有同类型任务在执行,请等待执行完成
|
||||
file_stem = Path(file_path).stem
|
||||
pdf_analysis_folder = current_app.config['PDF_ANALYSIS_FOLDER']
|
||||
pdf_dir = f"{current_app.static_folder}/{pdf_analysis_folder}/{file_stem}"
|
||||
image_dir = f"{pdf_dir}/images"
|
||||
t = threading.Thread(target=analysis_pdf_task,
|
||||
args=(pdf_dir, image_dir, file_path, is_ocr, analysis_pdf_id))
|
||||
t.start()
|
||||
# 生成文件的URL路径
|
||||
file_url = url_for('analysis.uploadpdfview', filename=file_name, as_attachment=False)
|
||||
data = {
|
||||
"url": file_url,
|
||||
"fileName": file_name,
|
||||
"id": analysis_task_id
|
||||
}
|
||||
return generate_response(data=data)
|
||||
case 'formula-detect':
|
||||
# if not file_path:
|
||||
# return generate_response(code=400, msg="FileKey is invalid, no image file found",
|
||||
# msgZH="fileKey无效,未找到图片")
|
||||
# return formula_detection(file_path, upload_dir)
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="功能待开发")
|
||||
case 'formula-extract':
|
||||
# if not file_path:
|
||||
# return generate_response(code=400, msg="FileKey is invalid, no image file found",
|
||||
# msgZH="fileKey无效,未找到图片")
|
||||
# return formula_recognition(file_path, upload_dir)
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="功能待开发")
|
||||
case 'table-recogn':
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="功能待开发")
|
||||
case _:
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="参数不支持")
|
||||
|
||||
def put(self):
|
||||
"""
|
||||
重新发起任务
|
||||
:return:
|
||||
"""
|
||||
params = json.loads(request.data)
|
||||
id = params.get('id')
|
||||
analysis_task = AnalysisTask.query.filter(AnalysisTask.id == id).first()
|
||||
match analysis_task.task_type:
|
||||
case 'pdf':
|
||||
task_r_p = AnalysisTask.query.filter(AnalysisTask.status.in_([0, 2])).first()
|
||||
if task_r_p:
|
||||
with db.auto_commit():
|
||||
analysis_pdf_object = AnalysisPdf.query.filter_by(id=analysis_task.analysis_pdf_id).first()
|
||||
analysis_pdf_object.status = 3
|
||||
db.session.add(analysis_pdf_object)
|
||||
with db.auto_commit():
|
||||
analysis_task.status = 2
|
||||
db.session.add(analysis_task)
|
||||
else:
|
||||
with db.auto_commit():
|
||||
analysis_pdf_object = AnalysisPdf.query.filter_by(id=analysis_task.analysis_pdf_id).first()
|
||||
analysis_pdf_object.status = 0
|
||||
db.session.add(analysis_pdf_object)
|
||||
with db.auto_commit():
|
||||
analysis_task.status = 0
|
||||
db.session.add(analysis_task)
|
||||
|
||||
pdf_upload_folder = current_app.config['PDF_UPLOAD_FOLDER']
|
||||
upload_dir = f"{current_app.static_folder}/{pdf_upload_folder}"
|
||||
file_path = find_file(analysis_task.file_key, upload_dir)
|
||||
file_stem = Path(file_path).stem
|
||||
pdf_analysis_folder = current_app.config['PDF_ANALYSIS_FOLDER']
|
||||
pdf_dir = f"{current_app.static_folder}/{pdf_analysis_folder}/{file_stem}"
|
||||
image_dir = f"{pdf_dir}/images"
|
||||
t = threading.Thread(target=analysis_pdf_task,
|
||||
args=(pdf_dir, image_dir, file_path, analysis_task.is_ocr,
|
||||
analysis_task.analysis_pdf_id))
|
||||
t.start()
|
||||
|
||||
# 生成文件的URL路径
|
||||
file_url = url_for('analysis.uploadpdfview', filename=analysis_task.file_name, as_attachment=False)
|
||||
data = {
|
||||
"url": file_url,
|
||||
"fileName": analysis_task.file_name,
|
||||
"id": analysis_task.id
|
||||
}
|
||||
return generate_response(data=data)
|
||||
case 'formula-detect':
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="功能待开发")
|
||||
case 'formula-extract':
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="功能待开发")
|
||||
case 'table-recogn':
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="功能待开发")
|
||||
case _:
|
||||
return generate_response(code=400, msg="Not yet supported", msgZH="参数不支持")
|
||||
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
task_state_map = {
|
||||
0: "running",
|
||||
1: "finished",
|
||||
2: "pending",
|
||||
}
|
||||
|
||||
|
||||
def find_file(file_key, file_dir):
|
||||
"""
|
||||
查询文件
|
||||
:param file_key: 文件哈希
|
||||
:param file_dir: 文件目录
|
||||
:return:
|
||||
"""
|
||||
pdf_path = ""
|
||||
for root, subDirs, files in os.walk(file_dir):
|
||||
for fileName in files:
|
||||
if fileName.startswith(file_key):
|
||||
pdf_path = os.path.join(root, fileName)
|
||||
break
|
||||
if pdf_path:
|
||||
break
|
||||
return pdf_path
|
||||
@@ -0,0 +1,280 @@
|
||||
import os
|
||||
import pkgutil
|
||||
import numpy as np
|
||||
import yaml
|
||||
import argparse
|
||||
import cv2
|
||||
from pathlib import Path
|
||||
from ultralytics import YOLO
|
||||
from unimernet.common.config import Config
|
||||
import unimernet.tasks as tasks
|
||||
from unimernet.processors import load_processor
|
||||
from magic_pdf.libs.config_reader import get_local_models_dir, get_device
|
||||
from torchvision import transforms
|
||||
from magic_pdf.pre_proc.ocr_span_list_modify import remove_overlaps_low_confidence_spans, remove_overlaps_min_spans
|
||||
from PIL import Image
|
||||
from common.ext import singleton_func
|
||||
from common.custom_response import generate_response
|
||||
|
||||
|
||||
def mfd_model_init(weight):
|
||||
mfd_model = YOLO(weight)
|
||||
return mfd_model
|
||||
|
||||
|
||||
def mfr_model_init(weight_dir, cfg_path, _device_='cpu'):
|
||||
args = argparse.Namespace(cfg_path=cfg_path, options=None)
|
||||
cfg = Config(args)
|
||||
cfg.config.model.pretrained = os.path.join(weight_dir, "pytorch_model.bin")
|
||||
cfg.config.model.model_config.model_name = weight_dir
|
||||
cfg.config.model.tokenizer_config.path = weight_dir
|
||||
task = tasks.setup_task(cfg)
|
||||
model = task.build_model(cfg)
|
||||
model = model.to(_device_)
|
||||
vis_processor = load_processor('formula_image_eval', cfg.config.datasets.formula_rec_eval.vis_processor.eval)
|
||||
return model, vis_processor
|
||||
|
||||
|
||||
@singleton_func
|
||||
class CustomPEKModel:
|
||||
def __init__(self):
|
||||
# PDF-Extract-Kit/models
|
||||
models_dir = get_local_models_dir()
|
||||
self.device = get_device()
|
||||
loader = pkgutil.get_loader("magic_pdf")
|
||||
root_dir = Path(loader.path).parent
|
||||
# model_config目录
|
||||
model_config_dir = os.path.join(root_dir, 'resources', 'model_config')
|
||||
# 构建 model_configs.yaml 文件的完整路径
|
||||
config_path = os.path.join(model_config_dir, 'model_configs.yaml')
|
||||
with open(config_path, "r", encoding='utf-8') as f:
|
||||
configs = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
# 初始化公式检测模型
|
||||
self.mfd_model = mfd_model_init(str(os.path.join(models_dir, configs["weights"]["mfd"])))
|
||||
|
||||
# 初始化公式解析模型
|
||||
mfr_weight_dir = str(os.path.join(models_dir, configs["weights"]["mfr"]))
|
||||
mfr_cfg_path = str(os.path.join(model_config_dir, "UniMERNet", "demo.yaml"))
|
||||
self.mfr_model, mfr_vis_processors = mfr_model_init(mfr_weight_dir, mfr_cfg_path, _device_=self.device)
|
||||
self.mfr_transform = transforms.Compose([mfr_vis_processors, ])
|
||||
|
||||
|
||||
def get_all_spans(layout_dets) -> list:
|
||||
def remove_duplicate_spans(spans):
|
||||
new_spans = []
|
||||
for span in spans:
|
||||
if not any(span == existing_span for existing_span in new_spans):
|
||||
new_spans.append(span)
|
||||
return new_spans
|
||||
|
||||
all_spans = []
|
||||
# allow_category_id_list = [3, 5, 13, 14, 15]
|
||||
"""当成span拼接的"""
|
||||
# 3: 'image', # 图片
|
||||
# 5: 'table', # 表格
|
||||
# 13: 'inline_equation', # 行内公式
|
||||
# 14: 'interline_equation', # 行间公式
|
||||
# 15: 'text', # ocr识别文本
|
||||
for layout_det in layout_dets:
|
||||
if layout_det.get("bbox") is not None:
|
||||
# 兼容直接输出bbox的模型数据,如paddle
|
||||
x0, y0, x1, y1 = layout_det["bbox"]
|
||||
else:
|
||||
# 兼容直接输出poly的模型数据,如xxx
|
||||
x0, y0, _, _, x1, y1, _, _ = layout_det["poly"]
|
||||
bbox = [x0, y0, x1, y1]
|
||||
layout_det["bbox"] = bbox
|
||||
all_spans.append(layout_det)
|
||||
return remove_duplicate_spans(all_spans)
|
||||
|
||||
|
||||
def formula_predict(mfd_model, image):
|
||||
"""
|
||||
公式检测
|
||||
:param mfd_model:
|
||||
:param image:
|
||||
:return:
|
||||
"""
|
||||
latex_filling_list = []
|
||||
# 公式检测
|
||||
mfd_res = mfd_model.predict(image, imgsz=1888, conf=0.25, iou=0.45, verbose=True)[0]
|
||||
for xyxy, conf, cla in zip(mfd_res.boxes.xyxy.cpu(), mfd_res.boxes.conf.cpu(), mfd_res.boxes.cls.cpu()):
|
||||
xmin, ymin, xmax, ymax = [int(p.item()) for p in xyxy]
|
||||
new_item = {
|
||||
'category_id': 13 + int(cla.item()),
|
||||
'poly': [xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax],
|
||||
'score': round(float(conf.item()), 2),
|
||||
'latex': '',
|
||||
}
|
||||
latex_filling_list.append(new_item)
|
||||
return latex_filling_list
|
||||
|
||||
|
||||
def formula_detection(file_path, upload_dir):
|
||||
"""
|
||||
公式检测
|
||||
:param file_path: 文件路径
|
||||
:param upload_dir: 上传文件夹
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
image_open = Image.open(file_path)
|
||||
except IOError:
|
||||
return generate_response(code=400, msg="params is not valid", msgZh="参数类型不是图片,无效参数")
|
||||
|
||||
filename = Path(file_path).name
|
||||
|
||||
# 获取图片宽高
|
||||
width, height = image_open.size
|
||||
# 转换为RGB,忽略透明度通道
|
||||
rgb_image = image_open.convert('RGB')
|
||||
# 保存转换后的图片
|
||||
rgb_image.save(file_path)
|
||||
|
||||
# 初始化模型
|
||||
cpm = CustomPEKModel()
|
||||
# 初始化公式检测模型
|
||||
mfd_model = cpm.mfd_model
|
||||
|
||||
image_conv = Image.open(file_path)
|
||||
image_array = np.array(image_conv)
|
||||
pdf_width = 1416
|
||||
pdf_height = 1888
|
||||
|
||||
# 重置图片大小
|
||||
scale = min(pdf_width // 2 / width, pdf_height // 2 / height) # 缩放比例
|
||||
nw = int(width * scale)
|
||||
nh = int(height * scale)
|
||||
image_resize = cv2.resize(image_array, (nw, nh), interpolation=cv2.INTER_LINEAR)
|
||||
resize_image_path = f"{upload_dir}/resize_{filename}"
|
||||
cv2.imwrite(resize_image_path, image_resize)
|
||||
# 将重置的图片贴到pdf白纸中
|
||||
x = (pdf_width - nw) // 2
|
||||
y = (pdf_height - nh) // 2
|
||||
new_img = Image.new('RGB', (pdf_width, pdf_height), 'white')
|
||||
image_scale = Image.open(resize_image_path)
|
||||
new_img.paste(image_scale, (x, y))
|
||||
|
||||
# 公式检测
|
||||
latex_filling_list = formula_predict(mfd_model, new_img)
|
||||
|
||||
os.remove(resize_image_path)
|
||||
|
||||
# 将缩放图公式检测的坐标还原为原图公式检测的坐标
|
||||
for item in latex_filling_list:
|
||||
item_poly = item["poly"]
|
||||
item["poly"] = [
|
||||
(item_poly[0] - x) / scale,
|
||||
(item_poly[1] - y) / scale,
|
||||
(item_poly[2] - x) / scale,
|
||||
(item_poly[3] - y) / scale,
|
||||
(item_poly[4] - x) / scale,
|
||||
(item_poly[5] - y) / scale,
|
||||
(item_poly[6] - x) / scale,
|
||||
(item_poly[7] - y) / scale,
|
||||
]
|
||||
|
||||
if not latex_filling_list:
|
||||
return generate_response(code=1001, msg="detection fail", msgZh="公式检测失败,图片过小,无法检测")
|
||||
|
||||
spans = get_all_spans(latex_filling_list)
|
||||
'''删除重叠spans中置信度较低的那些'''
|
||||
spans, dropped_spans_by_confidence = remove_overlaps_low_confidence_spans(spans)
|
||||
'''删除重叠spans中较小的那些'''
|
||||
spans, dropped_spans_by_span_overlap = remove_overlaps_min_spans(spans)
|
||||
|
||||
return generate_response(data={
|
||||
'layout': spans,
|
||||
})
|
||||
|
||||
|
||||
def formula_recognition(file_path, upload_dir):
|
||||
"""
|
||||
公式识别
|
||||
:param file_path: 文件路径
|
||||
:param upload_dir: 上传文件夹
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
image_open = Image.open(file_path)
|
||||
except IOError:
|
||||
return generate_response(code=400, msg="params is not valid", msgZh="参数类型不是图片,无效参数")
|
||||
|
||||
filename = Path(file_path).name
|
||||
|
||||
# 获取图片宽高
|
||||
width, height = image_open.size
|
||||
# 转换为RGB,忽略透明度通道
|
||||
rgb_image = image_open.convert('RGB')
|
||||
# 保存转换后的图片
|
||||
rgb_image.save(file_path)
|
||||
|
||||
image_conv = Image.open(file_path)
|
||||
image_array = np.array(image_conv)
|
||||
pdf_width = 1416
|
||||
pdf_height = 1888
|
||||
|
||||
# 重置图片大小
|
||||
scale = min(pdf_width // 2 / width, pdf_height // 2 / height) # 缩放比例
|
||||
nw = int(width * scale)
|
||||
nh = int(height * scale)
|
||||
image_resize = cv2.resize(image_array, (nw, nh), interpolation=cv2.INTER_LINEAR)
|
||||
resize_image_path = f"{upload_dir}/resize_{filename}"
|
||||
cv2.imwrite(resize_image_path, image_resize)
|
||||
# 将重置的图片贴到pdf白纸中
|
||||
x = (pdf_width - nw) // 2
|
||||
y = (pdf_height - nh) // 2
|
||||
new_img = Image.new('RGB', (pdf_width, pdf_height), 'white')
|
||||
image_scale = Image.open(resize_image_path)
|
||||
new_img.paste(image_scale, (x, y))
|
||||
new_img_array = np.array(new_img)
|
||||
|
||||
# 初始化模型
|
||||
cpm = CustomPEKModel()
|
||||
# device
|
||||
device = cpm.device
|
||||
# 初始化公式检测模型
|
||||
mfd_model = cpm.mfd_model
|
||||
# 初始化公式解析模型
|
||||
mfr_model = cpm.mfr_model
|
||||
mfr_transform = cpm.mfr_transform
|
||||
# 公式识别
|
||||
latex_filling_list, mfr_res = formula_recognition(mfd_model, new_img_array, mfr_transform, device, mfr_model,
|
||||
image_open)
|
||||
|
||||
os.remove(resize_image_path)
|
||||
|
||||
# 将缩放图公式检测的坐标还原为原图公式检测的坐标
|
||||
for item in latex_filling_list:
|
||||
item_poly = item["poly"]
|
||||
item["poly"] = [
|
||||
(item_poly[0] - x) / scale,
|
||||
(item_poly[1] - y) / scale,
|
||||
(item_poly[2] - x) / scale,
|
||||
(item_poly[3] - y) / scale,
|
||||
(item_poly[4] - x) / scale,
|
||||
(item_poly[5] - y) / scale,
|
||||
(item_poly[6] - x) / scale,
|
||||
(item_poly[7] - y) / scale,
|
||||
]
|
||||
|
||||
spans = get_all_spans(latex_filling_list)
|
||||
'''删除重叠spans中置信度较低的那些'''
|
||||
spans, dropped_spans_by_confidence = remove_overlaps_low_confidence_spans(spans)
|
||||
'''删除重叠spans中较小的那些'''
|
||||
spans, dropped_spans_by_span_overlap = remove_overlaps_min_spans(spans)
|
||||
|
||||
if not latex_filling_list:
|
||||
width, height = image_open.size
|
||||
latex_filling_list.append({
|
||||
'category_id': 14,
|
||||
'poly': [0, 0, width, 0, width, height, 0, height],
|
||||
'score': 1,
|
||||
'latex': mfr_res[0] if mfr_res else "",
|
||||
})
|
||||
|
||||
return generate_response(data={
|
||||
'layout': spans if spans else latex_filling_list,
|
||||
"mfr_res": mfr_res
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
from pathlib import Path
|
||||
from flask import request, current_app, send_from_directory
|
||||
from flask_restful import Resource
|
||||
|
||||
|
||||
class ImgView(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
获取pdf解析的图片
|
||||
:return:
|
||||
"""
|
||||
params = request.args
|
||||
pdf = params.get('pdf')
|
||||
filename = params.get('filename')
|
||||
as_attachment = params.get('as_attachment')
|
||||
if str(as_attachment).lower() == "true":
|
||||
as_attachment = True
|
||||
else:
|
||||
as_attachment = False
|
||||
file_stem = Path(pdf).stem
|
||||
pdf_analysis_folder = current_app.config['PDF_ANALYSIS_FOLDER']
|
||||
pdf_dir = f"{current_app.static_folder}/{pdf_analysis_folder}/{file_stem}"
|
||||
image_dir = f"{pdf_dir}/images"
|
||||
response = send_from_directory(image_dir, filename, as_attachment=as_attachment)
|
||||
return response
|
||||
|
||||
|
||||
class MdView(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
获取pdf解析的markdown
|
||||
:return:
|
||||
"""
|
||||
params = request.args
|
||||
pdf = params.get('pdf')
|
||||
filename = params.get('filename')
|
||||
as_attachment = params.get('as_attachment')
|
||||
if str(as_attachment).lower() == "true":
|
||||
as_attachment = True
|
||||
else:
|
||||
as_attachment = False
|
||||
file_stem = Path(pdf).stem
|
||||
pdf_analysis_folder = current_app.config['PDF_ANALYSIS_FOLDER']
|
||||
pdf_dir = f"{current_app.static_folder}/{pdf_analysis_folder}/{file_stem}"
|
||||
response = send_from_directory(pdf_dir, filename, as_attachment=as_attachment)
|
||||
return response
|
||||
@@ -0,0 +1,29 @@
|
||||
from datetime import datetime
|
||||
from ..extentions import db
|
||||
|
||||
|
||||
class AnalysisTask(db.Model):
|
||||
__tablename__ = 'analysis_task'
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
file_key = db.Column(db.Text, comment="文件唯一哈希")
|
||||
file_name = db.Column(db.Text, comment="文件名称")
|
||||
task_type = db.Column(db.String(128), comment="任务类型")
|
||||
is_ocr = db.Column(db.Boolean, default=False, comment="是否ocr")
|
||||
status = db.Column(db.Integer, default=0, comment="状态") # 0 running 1 finished 2 pending
|
||||
analysis_pdf_id = db.Column(db.Integer, comment="analysis_pdf的id")
|
||||
create_date = db.Column(db.DateTime(), nullable=False, default=datetime.now)
|
||||
update_date = db.Column(db.DateTime(), nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class AnalysisPdf(db.Model):
|
||||
__tablename__ = 'analysis_pdf'
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
file_name = db.Column(db.Text, comment="文件名称")
|
||||
file_url = db.Column(db.Text, comment="文件原路径")
|
||||
file_path = db.Column(db.Text, comment="文件路径")
|
||||
status = db.Column(db.Integer, default=3, comment="状态") # 0 转换中 1 已完成 2 转换失败 3 init
|
||||
bbox_info = db.Column(db.Text, comment="坐标数据")
|
||||
md_link_list = db.Column(db.Text, comment="markdown分页链接")
|
||||
full_md_link = db.Column(db.Text, comment="markdown全文链接")
|
||||
create_date = db.Column(db.DateTime(), nullable=False, default=datetime.now)
|
||||
update_date = db.Column(db.DateTime(), nullable=False, default=datetime.now, onupdate=datetime.now)
|
||||
@@ -0,0 +1,162 @@
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from flask import current_app, url_for
|
||||
from magic_pdf.rw.DiskReaderWriter import DiskReaderWriter
|
||||
from magic_pdf.pipe.UNIPipe import UNIPipe
|
||||
import magic_pdf.model as model_config
|
||||
from magic_pdf.libs.json_compressor import JsonCompressor
|
||||
from magic_pdf.dict2md.ocr_mkcontent import ocr_mk_mm_markdown_with_para_and_pagination
|
||||
from .ext import find_file
|
||||
from ..extentions import app, db
|
||||
from .models import AnalysisPdf, AnalysisTask
|
||||
from common.error_types import ApiException
|
||||
from loguru import logger
|
||||
|
||||
model_config.__use_inside_model__ = True
|
||||
|
||||
|
||||
def analysis_pdf(image_dir, pdf_bytes, is_ocr=False):
|
||||
try:
|
||||
model_json = [] # model_json传空list使用内置模型解析
|
||||
logger.info(f"is_ocr: {is_ocr}")
|
||||
if not is_ocr:
|
||||
jso_useful_key = {"_pdf_type": "", "model_list": model_json}
|
||||
image_writer = DiskReaderWriter(image_dir)
|
||||
pipe = UNIPipe(pdf_bytes, jso_useful_key, image_writer, is_debug=True)
|
||||
pipe.pipe_classify()
|
||||
else:
|
||||
jso_useful_key = {"_pdf_type": "ocr", "model_list": model_json}
|
||||
image_writer = DiskReaderWriter(image_dir)
|
||||
pipe = UNIPipe(pdf_bytes, jso_useful_key, image_writer, is_debug=True)
|
||||
"""如果没有传入有效的模型数据,则使用内置model解析"""
|
||||
if len(model_json) == 0:
|
||||
if model_config.__use_inside_model__:
|
||||
pipe.pipe_analyze()
|
||||
else:
|
||||
logger.error("need model list input")
|
||||
exit(1)
|
||||
pipe.pipe_parse()
|
||||
pdf_mid_data = JsonCompressor.decompress_json(pipe.get_compress_pdf_mid_data())
|
||||
pdf_info_list = pdf_mid_data["pdf_info"]
|
||||
md_content = json.dumps(ocr_mk_mm_markdown_with_para_and_pagination(pdf_info_list, image_dir),
|
||||
ensure_ascii=False)
|
||||
bbox_info = get_bbox_info(pdf_info_list)
|
||||
return md_content, bbox_info
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def get_bbox_info(data):
|
||||
bbox_info = []
|
||||
for page in data:
|
||||
preproc_blocks = page.get("preproc_blocks", [])
|
||||
discarded_blocks = page.get("discarded_blocks", [])
|
||||
bbox_info.append({
|
||||
"preproc_blocks": preproc_blocks,
|
||||
"page_idx": page.get("page_idx"),
|
||||
"page_size": page.get("page_size"),
|
||||
"discarded_blocks": discarded_blocks,
|
||||
})
|
||||
return bbox_info
|
||||
|
||||
|
||||
def analysis_pdf_task(pdf_dir, image_dir, pdf_path, is_ocr, analysis_pdf_id):
|
||||
"""
|
||||
解析pdf
|
||||
:param pdf_dir: pdf解析目录
|
||||
:param image_dir: 图片目录
|
||||
:param pdf_path: pdf路径
|
||||
:param is_ocr: 是否启用ocr
|
||||
:param analysis_pdf_id: pdf解析表id
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
logger.info(f"start task: {pdf_path}")
|
||||
logger.info(f"image_dir: {image_dir}")
|
||||
if not Path(image_dir).exists():
|
||||
Path(image_dir).mkdir(parents=True, exist_ok=True)
|
||||
with open(pdf_path, 'rb') as file:
|
||||
pdf_bytes = file.read()
|
||||
md_content, bbox_info = analysis_pdf(image_dir, pdf_bytes, is_ocr)
|
||||
img_list = Path(image_dir).glob('*') if Path(image_dir).exists() else []
|
||||
|
||||
pdf_name = Path(pdf_path).name
|
||||
with app.app_context():
|
||||
for img in img_list:
|
||||
img_name = Path(img).name
|
||||
regex = re.compile(fr'.*\((.*?{img_name})')
|
||||
regex_result = regex.search(md_content)
|
||||
img_url = url_for('analysis.imgview', filename=img_name, as_attachment=False)
|
||||
md_content = md_content.replace(regex_result.group(1), f"{img_url}&pdf={pdf_name}")
|
||||
|
||||
full_md_content = ""
|
||||
for item in json.loads(md_content):
|
||||
full_md_content += item["md_content"] + "\n"
|
||||
|
||||
full_md_name = "full.md"
|
||||
with open(f"{pdf_dir}/{full_md_name}", "w") as file:
|
||||
file.write(full_md_content)
|
||||
with app.app_context():
|
||||
full_md_link = url_for('analysis.mdview', filename=full_md_name, as_attachment=False)
|
||||
full_md_link = f"{full_md_link}&pdf={pdf_name}"
|
||||
|
||||
md_link_list = []
|
||||
with app.app_context():
|
||||
for n, md in enumerate(json.loads(md_content)):
|
||||
md_content = md["md_content"]
|
||||
md_name = f"{md.get('page_no', n)}.md"
|
||||
with open(f"{pdf_dir}/{md_name}", "w") as file:
|
||||
file.write(md_content)
|
||||
md_url = url_for('analysis.mdview', filename=md_name, as_attachment=False)
|
||||
md_link_list.append(f"{md_url}&pdf={pdf_name}")
|
||||
|
||||
with app.app_context():
|
||||
with db.auto_commit():
|
||||
analysis_pdf_object = AnalysisPdf.query.filter_by(id=analysis_pdf_id).first()
|
||||
analysis_pdf_object.status = 1
|
||||
analysis_pdf_object.bbox_info = json.dumps(bbox_info, ensure_ascii=False)
|
||||
analysis_pdf_object.md_link_list = json.dumps(md_link_list, ensure_ascii=False)
|
||||
analysis_pdf_object.full_md_link = full_md_link
|
||||
db.session.add(analysis_pdf_object)
|
||||
with db.auto_commit():
|
||||
analysis_task_object = AnalysisTask.query.filter_by(analysis_pdf_id=analysis_pdf_id).first()
|
||||
analysis_task_object.status = 1
|
||||
db.session.add(analysis_task_object)
|
||||
logger.info(f"finished!")
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
with app.app_context():
|
||||
with db.auto_commit():
|
||||
analysis_pdf_object = AnalysisPdf.query.filter_by(id=analysis_pdf_id).first()
|
||||
analysis_pdf_object.status = 2
|
||||
db.session.add(analysis_pdf_object)
|
||||
with db.auto_commit():
|
||||
analysis_task_object = AnalysisTask.query.filter_by(analysis_pdf_id=analysis_pdf_id).first()
|
||||
analysis_task_object.status = 1
|
||||
db.session.add(analysis_task_object)
|
||||
raise ApiException(code=500, msg="PDF parsing failed", msgZH="pdf解析失败")
|
||||
finally:
|
||||
# 执行pending
|
||||
with app.app_context():
|
||||
analysis_task_object = AnalysisTask.query.filter_by(status=2).order_by(
|
||||
AnalysisTask.update_date.asc()).first()
|
||||
if analysis_task_object:
|
||||
pdf_upload_folder = current_app.config['PDF_UPLOAD_FOLDER']
|
||||
upload_dir = f"{current_app.static_folder}/{pdf_upload_folder}"
|
||||
file_path = find_file(analysis_task_object.file_key, upload_dir)
|
||||
file_stem = Path(file_path).stem
|
||||
pdf_analysis_folder = current_app.config['PDF_ANALYSIS_FOLDER']
|
||||
pdf_dir = f"{current_app.static_folder}/{pdf_analysis_folder}/{file_stem}"
|
||||
image_dir = f"{pdf_dir}/images"
|
||||
with db.auto_commit():
|
||||
analysis_pdf_object = AnalysisPdf.query.filter_by(id=analysis_task_object.analysis_pdf_id).first()
|
||||
analysis_pdf_object.status = 0
|
||||
db.session.add(analysis_pdf_object)
|
||||
with db.auto_commit():
|
||||
analysis_task_object.status = 0
|
||||
db.session.add(analysis_task_object)
|
||||
analysis_pdf_task(pdf_dir, image_dir, file_path, analysis_task_object.is_ocr, analysis_task_object.analysis_pdf_id)
|
||||
else:
|
||||
logger.info(f"all task finished!")
|
||||
@@ -0,0 +1,28 @@
|
||||
from marshmallow import Schema, fields, validates_schema, validates
|
||||
from common.error_types import ApiException
|
||||
from .models import AnalysisTask
|
||||
|
||||
|
||||
class BooleanField(fields.Boolean):
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
# 进行自定义验证
|
||||
if not isinstance(value, bool):
|
||||
raise ApiException(code=400, msg="isOcr not a valid boolean", msgZH="isOcr不是有效的布尔值")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class AnalysisViewSchema(Schema):
|
||||
fileKey = fields.Str(required=True)
|
||||
fileName = fields.Str()
|
||||
taskType = fields.Str(required=True)
|
||||
isOcr = BooleanField()
|
||||
|
||||
@validates_schema(pass_many=True)
|
||||
def validate_passwords(self, data, **kwargs):
|
||||
task_type = data['taskType']
|
||||
file_key = data['fileKey']
|
||||
if not file_key:
|
||||
raise ApiException(code=400, msg="fileKey cannot be empty", msgZH="fileKey不能为空")
|
||||
if not task_type:
|
||||
raise ApiException(code=400, msg="taskType cannot be empty", msgZH="taskType不能为空")
|
||||
@@ -0,0 +1,95 @@
|
||||
import json
|
||||
from flask import url_for, request
|
||||
from flask_restful import Resource
|
||||
from sqlalchemy import func
|
||||
from ..extentions import db
|
||||
from .models import AnalysisTask, AnalysisPdf
|
||||
from .ext import task_state_map
|
||||
from common.custom_response import generate_response
|
||||
|
||||
|
||||
class TaskView(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
查询正在进行的任务
|
||||
:return:
|
||||
"""
|
||||
analysis_task_running = AnalysisTask.query.filter(AnalysisTask.status == 0).first()
|
||||
analysis_task_pending = AnalysisTask.query.filter(AnalysisTask.status == 2).order_by(
|
||||
AnalysisTask.create_date.asc()).all()
|
||||
pending_total = db.session.query(func.count(AnalysisTask.id)).filter(AnalysisTask.status == 2).scalar()
|
||||
task_nums = pending_total + 1
|
||||
data = [
|
||||
{
|
||||
"queues": task_nums, # 正在排队的任务总数
|
||||
"rank": 1,
|
||||
"id": analysis_task_running.id,
|
||||
"url": url_for('analysis.uploadpdfview', filename=analysis_task_running.file_name, as_attachment=False),
|
||||
"fileName": analysis_task_running.file_name,
|
||||
"type": analysis_task_running.task_type,
|
||||
"state": task_state_map.get(analysis_task_running.status),
|
||||
}
|
||||
]
|
||||
for n, task in enumerate(analysis_task_pending):
|
||||
data.append({
|
||||
"queues": task_nums, # 正在排队的任务总数
|
||||
"rank": n + 2,
|
||||
"id": task.id,
|
||||
"url": url_for('analysis.uploadpdfview', filename=task.file_name, as_attachment=False),
|
||||
"fileName": task.file_name,
|
||||
"type": task.task_type,
|
||||
"state": task_state_map.get(task.status),
|
||||
})
|
||||
data.reverse()
|
||||
return generate_response(data=data, total=task_nums)
|
||||
|
||||
|
||||
class HistoricalTasksView(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
获取任务历史记录
|
||||
:return:
|
||||
"""
|
||||
params = request.args
|
||||
page_no = params.get('pageNo', 1)
|
||||
page_size = params.get('pageSize', 10)
|
||||
total = db.session.query(func.count(AnalysisTask.id)).scalar()
|
||||
analysis_task = AnalysisTask.query.order_by(AnalysisTask.create_date.desc()).paginate(page=int(page_no),
|
||||
per_page=int(page_size),
|
||||
error_out=False)
|
||||
data = []
|
||||
for n, task in enumerate(analysis_task):
|
||||
data.append({
|
||||
"fileName": task.file_name,
|
||||
"id": task.id,
|
||||
"type": task.task_type,
|
||||
"state": task_state_map.get(task.status),
|
||||
})
|
||||
data = {
|
||||
"list": data,
|
||||
"total": total,
|
||||
"pageNo": page_no,
|
||||
"pageSize": page_size,
|
||||
}
|
||||
return generate_response(data=data)
|
||||
|
||||
|
||||
class DeleteTaskView(Resource):
|
||||
def delete(self):
|
||||
"""
|
||||
删除任务历史记录
|
||||
:return:
|
||||
"""
|
||||
params = json.loads(request.data)
|
||||
id = params.get('id')
|
||||
|
||||
analysis_task = AnalysisTask.query.filter(AnalysisTask.id == id, AnalysisTask.status != 0).first()
|
||||
if analysis_task:
|
||||
analysis_pdf = AnalysisPdf.query.filter(AnalysisPdf.id == AnalysisTask.analysis_pdf_id).first()
|
||||
with db.auto_commit():
|
||||
db.session.delete(analysis_pdf)
|
||||
db.session.delete(analysis_task)
|
||||
else:
|
||||
return generate_response(code=400, msg="The ID is incorrect", msgZH="id不正确")
|
||||
|
||||
return generate_response(data={"id": id})
|
||||
@@ -0,0 +1,89 @@
|
||||
import json
|
||||
import traceback
|
||||
import requests
|
||||
from flask import request, current_app, url_for, send_from_directory
|
||||
from flask_restful import Resource
|
||||
from werkzeug.utils import secure_filename
|
||||
from pathlib import Path
|
||||
from common.ext import is_pdf, calculate_file_hash, url_is_pdf
|
||||
from io import BytesIO
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from common.custom_response import generate_response
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class UploadPdfView(Resource):
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
获取pdf
|
||||
:return:
|
||||
"""
|
||||
params = request.args
|
||||
filename = params.get('filename')
|
||||
as_attachment = params.get('as_attachment')
|
||||
if str(as_attachment).lower() == "true":
|
||||
as_attachment = True
|
||||
else:
|
||||
as_attachment = False
|
||||
pdf_upload_folder = current_app.config['PDF_UPLOAD_FOLDER']
|
||||
response = send_from_directory(f"{current_app.static_folder}/{pdf_upload_folder}", filename,
|
||||
as_attachment=as_attachment)
|
||||
return response
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
上传pdf
|
||||
:return:
|
||||
"""
|
||||
file_list = request.files.getlist("file")
|
||||
if file_list:
|
||||
file = file_list[0]
|
||||
filename = secure_filename(file.filename)
|
||||
if not file or file and not is_pdf(filename, file):
|
||||
return generate_response(code=400, msg="Invalid PDF file", msgZH="PDF文件参数无效")
|
||||
else:
|
||||
params = json.loads(request.data)
|
||||
pdf_url = params.get('pdfUrl')
|
||||
try:
|
||||
response = requests.get(pdf_url, stream=True)
|
||||
except ConnectionError as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return generate_response(code=400, msg="params is not valid", msgZh="参数错误,pdf链接无法访问")
|
||||
if response.status_code != 200:
|
||||
return generate_response(code=400, msg="params is not valid", msgZh="参数错误,pdf链接响应状态异常")
|
||||
# 创建一个模拟的 FileStorage 对象
|
||||
file_content = BytesIO(response.content)
|
||||
filename = Path(pdf_url).name if ".pdf" in pdf_url else f"{Path(pdf_url).name}.pdf"
|
||||
file = FileStorage(
|
||||
stream=file_content,
|
||||
filename=filename,
|
||||
content_type=response.headers.get('Content-Type', 'application/octet-stream')
|
||||
)
|
||||
if not file or file and not url_is_pdf(file):
|
||||
return generate_response(code=400, msg="Invalid PDF file", msgZH="PDF文件参数无效")
|
||||
|
||||
pdf_upload_folder = current_app.config['PDF_UPLOAD_FOLDER']
|
||||
upload_dir = f"{current_app.static_folder}/{pdf_upload_folder}"
|
||||
if not Path(upload_dir).exists():
|
||||
Path(upload_dir).mkdir(parents=True, exist_ok=True)
|
||||
file_key = calculate_file_hash(file)
|
||||
# new_filename = f"{int(time.time())}_{filename}"
|
||||
new_filename = f"{file_key}_{filename}"
|
||||
file_path = f"{upload_dir}/{new_filename}"
|
||||
# file.save(file_path)
|
||||
chunk_size = 8192
|
||||
with open(file_path, 'wb') as f:
|
||||
while True:
|
||||
chunk = file.stream.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
|
||||
# 生成文件的URL路径
|
||||
file_url = url_for('analysis.uploadpdfview', filename=new_filename, as_attachment=False)
|
||||
data = {
|
||||
"url": file_url,
|
||||
"file_key": file_key
|
||||
}
|
||||
return generate_response(data=data)
|
||||
@@ -0,0 +1,61 @@
|
||||
from flask import Flask, jsonify
|
||||
from flask_restful import Api as _Api
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from contextlib import contextmanager
|
||||
from flask_jwt_extended import JWTManager
|
||||
from flask_marshmallow import Marshmallow
|
||||
from common.error_types import ApiException
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class Api(_Api):
|
||||
def handle_error(self, e):
|
||||
if isinstance(e, ApiException):
|
||||
code = e.code
|
||||
msg = e.msg
|
||||
msgZH = e.msgZH
|
||||
error_code = e.error_code
|
||||
elif isinstance(e, HTTPException):
|
||||
code = e.code
|
||||
msg = e.description
|
||||
msgZH = "服务异常,详细信息请查看日志"
|
||||
error_code = e.code
|
||||
else:
|
||||
code = 500
|
||||
msg = str(e)
|
||||
error_code = 500
|
||||
msgZH = "服务异常,详细信息请查看日志"
|
||||
|
||||
# 使用 loguru 记录异常信息
|
||||
logger.opt(exception=e).error(f"An error occurred: {msg}")
|
||||
|
||||
return jsonify({
|
||||
"error": "Internal Server Error" if code == 500 else e.name,
|
||||
"msg": msg,
|
||||
"msgZH": msgZH,
|
||||
"code": code,
|
||||
"error_code": error_code
|
||||
}), code
|
||||
|
||||
|
||||
class SQLAlchemy(_SQLAlchemy):
|
||||
@contextmanager
|
||||
def auto_commit(self):
|
||||
try:
|
||||
yield
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, supports_credentials=True)
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
jwt = JWTManager()
|
||||
ma = Marshmallow()
|
||||
@@ -0,0 +1,54 @@
|
||||
import socket
|
||||
from api import create_app
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
|
||||
def get_local_ip():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.connect(('8.8.8.8', 80)) # Google DNS 服务器
|
||||
ip_address = sock.getsockname()[0]
|
||||
sock.close()
|
||||
return ip_address
|
||||
|
||||
|
||||
current_file_path = Path(__file__).resolve()
|
||||
base_dir = current_file_path.parent
|
||||
config_path = base_dir / "config/config.yaml"
|
||||
|
||||
|
||||
class ConfigMap(dict):
|
||||
__setattr__ = dict.__setitem__
|
||||
__getattr__ = dict.__getitem__
|
||||
|
||||
|
||||
with open(str(config_path), mode='r', encoding='utf-8') as fd:
|
||||
data = yaml.load(fd, Loader=yaml.FullLoader)
|
||||
_config = data.get(data.get("CurrentConfig", "DevelopmentConfig"))
|
||||
config = ConfigMap()
|
||||
for k, v in _config.items():
|
||||
config[k] = v
|
||||
config['base_dir'] = base_dir
|
||||
database = _config.get("database")
|
||||
if database:
|
||||
if database.get("type") == "sqlite":
|
||||
database_uri = f'sqlite:///{base_dir}/{database.get("path")}'
|
||||
elif database.get("type") == "mysql":
|
||||
database_uri = f'mysql+pymysql://{database.get("user")}:{database.get("password")}@{database.get("host")}:{database.get("port")}/{database.get("database")}?'
|
||||
else:
|
||||
database_uri = ''
|
||||
config['SQLALCHEMY_DATABASE_URI'] = database_uri
|
||||
|
||||
ip_address = get_local_ip()
|
||||
port = config.get("PORT", 5559)
|
||||
# 配置 SERVER_NAME
|
||||
config['SERVER_NAME'] = f'{ip_address}:5559'
|
||||
# 配置 APPLICATION_ROOT
|
||||
config['APPLICATION_ROOT'] = '/'
|
||||
# 配置 PREFERRED_URL_SCHEME
|
||||
config['PREFERRED_URL_SCHEME'] = 'http'
|
||||
|
||||
app = create_app(config)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=port, debug=config.get("DEBUG", False))
|
||||
@@ -0,0 +1,23 @@
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
class ResponseCode:
|
||||
SUCCESS = 200
|
||||
PARAM_WARING = 400
|
||||
MESSAGE = "success"
|
||||
|
||||
|
||||
def generate_response(data=None, code=ResponseCode.SUCCESS, msg=ResponseCode.MESSAGE, **kwargs):
|
||||
"""
|
||||
自定义响应
|
||||
:param code:状态码
|
||||
:param data:返回数据
|
||||
:param msg:返回消息
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
msg = msg or 'success' if code == 200 else msg or 'fail'
|
||||
success = True if code == 200 else False
|
||||
res = jsonify(dict(code=code, success=success, data=data, msg=msg, **kwargs))
|
||||
res.status_code = 200
|
||||
return res
|
||||
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
from flask import request
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
|
||||
class ApiException(HTTPException):
|
||||
"""API错误基类"""
|
||||
code = 500
|
||||
msg = 'Sorry, we made a mistake Σ(っ °Д °;)っ'
|
||||
msgZH = ""
|
||||
error_code = 999
|
||||
|
||||
def __init__(self, msg=None, msgZH=None, code=None, error_code=None, headers=None):
|
||||
if code:
|
||||
self.code = code
|
||||
if msg:
|
||||
self.msg = msg
|
||||
if msgZH:
|
||||
self.msgZH = msgZH
|
||||
if error_code:
|
||||
self.error_code = error_code
|
||||
super(ApiException, self).__init__(msg, None)
|
||||
|
||||
@staticmethod
|
||||
def get_error_url():
|
||||
"""获取出错路由和请求方式"""
|
||||
method = request.method
|
||||
full_path = str(request.full_path)
|
||||
main_path = full_path.split('?')[0]
|
||||
res = method + ' ' + main_path
|
||||
return res
|
||||
|
||||
def get_body(self, environ=None, scope=None):
|
||||
"""异常返回信息"""
|
||||
body = dict(
|
||||
msg=self.msg,
|
||||
error_code=self.error_code,
|
||||
request=self.get_error_url()
|
||||
)
|
||||
text = json.dumps(body)
|
||||
return text
|
||||
|
||||
def get_headers(self, environ=None, scope=None):
|
||||
"""异常返回格式"""
|
||||
return [("Content-Type", "application/json")]
|
||||
@@ -0,0 +1,80 @@
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
|
||||
def is_pdf(filename, file):
|
||||
"""
|
||||
判断文件是否为PDF格式。
|
||||
|
||||
:param filename: 文件名
|
||||
:param file: 文件对象
|
||||
:return: 如果文件是PDF格式,则返回True,否则返回False
|
||||
"""
|
||||
# 检查文件扩展名 https://arxiv.org/pdf/2405.08702 pdf链接可能存在不带扩展名的情况,先注释
|
||||
if not filename.endswith('.pdf'):
|
||||
return False
|
||||
|
||||
# 检查MIME类型
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
print(mime_type)
|
||||
if mime_type != 'application/pdf':
|
||||
return False
|
||||
|
||||
# 可选:读取文件的前几KB内容并检查MIME类型
|
||||
# 这一步是可选的,用于更严格的检查
|
||||
# if not mimetypes.guess_type(filename, strict=False)[0] == 'application/pdf':
|
||||
# return False
|
||||
|
||||
# 检查文件内容
|
||||
file_start = file.read(5)
|
||||
file.seek(0)
|
||||
if not file_start.startswith(b'%PDF-'):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def url_is_pdf(file):
|
||||
"""
|
||||
判断文件是否为PDF格式。
|
||||
|
||||
:param file: 文件对象
|
||||
:return: 如果文件是PDF格式,则返回True,否则返回False
|
||||
"""
|
||||
# 检查文件内容
|
||||
file_start = file.read(5)
|
||||
file.seek(0)
|
||||
if not file_start.startswith(b'%PDF-'):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def calculate_file_hash(file, algorithm='sha256'):
|
||||
"""
|
||||
计算给定文件的哈希值。
|
||||
|
||||
:param file: 文件对象
|
||||
:param algorithm: 哈希算法的名字,如:'sha256', 'md5', 'sha1'等
|
||||
:return: 文件的哈希值
|
||||
"""
|
||||
hash_func = getattr(hashlib, algorithm)()
|
||||
block_size = 65536 # 64KB chunks
|
||||
# with open(file_path, 'rb') as file:
|
||||
buffer = file.read(block_size)
|
||||
while len(buffer) > 0:
|
||||
hash_func.update(buffer)
|
||||
buffer = file.read(block_size)
|
||||
file.seek(0)
|
||||
return hash_func.hexdigest()
|
||||
|
||||
|
||||
def singleton_func(cls):
|
||||
instance = {}
|
||||
|
||||
def _singleton(*args, **kwargs):
|
||||
if cls not in instance:
|
||||
instance[cls] = cls(*args, **kwargs)
|
||||
return instance[cls]
|
||||
|
||||
return _singleton
|
||||
@@ -0,0 +1 @@
|
||||
from api.analysis.models import *
|
||||
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def setup_log(config):
|
||||
"""
|
||||
Setup logging
|
||||
:param config: config file
|
||||
:return:
|
||||
"""
|
||||
log_path = os.path.join(Path(__file__).parent.parent, "log")
|
||||
if not Path(log_path).exists():
|
||||
Path(log_path).mkdir(parents=True, exist_ok=True)
|
||||
log_level = config.get("LOG_LEVEL")
|
||||
log_name = f'log_{datetime.now().strftime("%Y-%m-%d")}.log'
|
||||
log_file_path = os.path.join(log_path, log_name)
|
||||
logger.add(str(log_file_path), rotation='00:00', encoding='utf-8', level=log_level, enqueue=True)
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
def before_request():
|
||||
return None
|
||||
|
||||
|
||||
def after_request(response):
|
||||
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
|
||||
return response
|
||||
@@ -0,0 +1,31 @@
|
||||
# 基本配置
|
||||
BaseConfig: &base
|
||||
DEBUG: false
|
||||
PORT: 5559
|
||||
LOG_LEVEL: "DEBUG"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS: true
|
||||
SQLALCHEMY_DATABASE_URI: ""
|
||||
PROPAGATE_EXCEPTIONS: true
|
||||
SECRET_KEY: "#$%^&**$##*(*^%%$**((&"
|
||||
JWT_SECRET_KEY: "#$%^&**$##*(*^%%$**((&"
|
||||
JWT_ACCESS_TOKEN_EXPIRES: 3600
|
||||
PDF_UPLOAD_FOLDER: "upload_pdf"
|
||||
PDF_ANALYSIS_FOLDER: "analysis_pdf"
|
||||
|
||||
# 开发配置
|
||||
DevelopmentConfig:
|
||||
<<: *base
|
||||
database:
|
||||
type: sqlite
|
||||
path: config/mineru_web.db
|
||||
|
||||
# 生产配置
|
||||
ProductionConfig:
|
||||
<<: *base
|
||||
|
||||
# 测试配置
|
||||
TestingConfig:
|
||||
<<: *base
|
||||
|
||||
# 当前使用配置
|
||||
CurrentConfig: "DevelopmentConfig"
|
||||
Binary file not shown.
Reference in New Issue
Block a user