Aryan Jain commited on
Commit
12874e6
·
1 Parent(s): 3e28a11

apis for sows

Browse files
Dockerfile CHANGED
@@ -13,6 +13,8 @@ WORKDIR /app
13
  RUN apt-get update && apt-get install -y \
14
  curl \
15
  build-essential \
 
 
16
  && rm -rf /var/lib/apt/lists/*
17
 
18
  # Install Poetry
 
13
  RUN apt-get update && apt-get install -y \
14
  curl \
15
  build-essential \
16
+ tesseract-ocr \
17
+ libtesseract-dev \
18
  && rm -rf /var/lib/apt/lists/*
19
 
20
  # Install Poetry
alembic/versions/3cb64e2750d4_add_tables.py CHANGED
@@ -1,10 +1,11 @@
1
  """add tables
2
 
3
  Revision ID: 3cb64e2750d4
4
- Revises:
5
  Create Date: 2025-06-06 15:48:03.057479
6
 
7
  """
 
8
  from typing import Sequence, Union
9
 
10
  from alembic import op
@@ -14,7 +15,7 @@ from sqlalchemy.dialects import postgresql
14
 
15
 
16
  # revision identifiers, used by Alembic.
17
- revision: str = '3cb64e2750d4'
18
  down_revision: Union[str, None] = None
19
  branch_labels: Union[str, Sequence[str], None] = None
20
  depends_on: Union[str, Sequence[str], None] = None
@@ -27,29 +28,24 @@ def upgrade() -> None:
27
  sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
28
  sa.Column("solicitation_no", sa.String(), nullable=False),
29
  sa.Column("solicitation_title", sa.String(), nullable=False),
30
- sa.Column("status", sa.Enum("COMPLETED", "IN_PROGRESS", "PENDING", name="rfpstatus"), nullable=False, default="PENDING"),
 
 
 
 
 
31
  sa.Column("google_drive_id", sa.String(), nullable=False),
32
  sa.Column("created_at", sa.DateTime(), nullable=False),
33
  sa.Column("updated_at", sa.DateTime(), nullable=False),
34
  )
35
 
36
  op.create_table(
37
- "sow_requirements",
38
  sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
39
  sa.Column("rfp_id", postgresql.UUID(as_uuid=True), nullable=False, unique=True),
40
  sa.Column("requirement", sa.String(), nullable=True),
41
  sa.Column("additional_info", sa.String(), nullable=True),
42
- sa.Column("created_at", sa.DateTime(), nullable=False),
43
- sa.Column("updated_at", sa.DateTime(), nullable=False),
44
- sa.ForeignKeyConstraint(["rfp_id"], ["rfps.id"], ondelete="CASCADE"),
45
- )
46
-
47
- op.create_table(
48
- "generated_sows",
49
- sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
50
- sa.Column("rfp_id", postgresql.UUID(as_uuid=True), nullable=False, unique=True),
51
- sa.Column("sow_text", sa.String(), nullable=False),
52
- sa.Column("google_drive_id", sa.String(), nullable=False),
53
  sa.Column("created_at", sa.DateTime(), nullable=False),
54
  sa.Column("updated_at", sa.DateTime(), nullable=False),
55
  sa.ForeignKeyConstraint(["rfp_id"], ["rfps.id"], ondelete="CASCADE"),
@@ -59,5 +55,4 @@ def upgrade() -> None:
59
  def downgrade() -> None:
60
  """Downgrade schema."""
61
  op.drop_table("generated_sows")
62
- op.drop_table("sow_requirements")
63
- op.drop_table("rfps")
 
1
  """add tables
2
 
3
  Revision ID: 3cb64e2750d4
4
+ Revises:
5
  Create Date: 2025-06-06 15:48:03.057479
6
 
7
  """
8
+
9
  from typing import Sequence, Union
10
 
11
  from alembic import op
 
15
 
16
 
17
  # revision identifiers, used by Alembic.
18
+ revision: str = "3cb64e2750d4"
19
  down_revision: Union[str, None] = None
20
  branch_labels: Union[str, Sequence[str], None] = None
21
  depends_on: Union[str, Sequence[str], None] = None
 
28
  sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
29
  sa.Column("solicitation_no", sa.String(), nullable=False),
30
  sa.Column("solicitation_title", sa.String(), nullable=False),
31
+ sa.Column(
32
+ "status",
33
+ sa.Enum("COMPLETED", "IN_PROGRESS", "PENDING", name="rfpstatus"),
34
+ nullable=False,
35
+ default="PENDING",
36
+ ),
37
  sa.Column("google_drive_id", sa.String(), nullable=False),
38
  sa.Column("created_at", sa.DateTime(), nullable=False),
39
  sa.Column("updated_at", sa.DateTime(), nullable=False),
40
  )
41
 
42
  op.create_table(
43
+ "sows",
44
  sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
45
  sa.Column("rfp_id", postgresql.UUID(as_uuid=True), nullable=False, unique=True),
46
  sa.Column("requirement", sa.String(), nullable=True),
47
  sa.Column("additional_info", sa.String(), nullable=True),
48
+ sa.Column("sow_generated_text", sa.String(), nullable=True),
 
 
 
 
 
 
 
 
 
 
49
  sa.Column("created_at", sa.DateTime(), nullable=False),
50
  sa.Column("updated_at", sa.DateTime(), nullable=False),
51
  sa.ForeignKeyConstraint(["rfp_id"], ["rfps.id"], ondelete="CASCADE"),
 
55
  def downgrade() -> None:
56
  """Downgrade schema."""
57
  op.drop_table("generated_sows")
58
+ op.drop_table("sows")
 
poetry.lock CHANGED
@@ -1,5 +1,16 @@
1
  # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
2
 
 
 
 
 
 
 
 
 
 
 
 
3
  [[package]]
4
  name = "aiosqlite"
5
  version = "0.21.0"
@@ -132,6 +143,28 @@ docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"]
132
  gssauth = ["gssapi", "sspilib"]
133
  test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"]
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  [[package]]
136
  name = "click"
137
  version = "8.2.1"
@@ -342,6 +375,154 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
342
  [package.extras]
343
  dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"]
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  [[package]]
346
  name = "mako"
347
  version = "1.3.10"
@@ -361,6 +542,21 @@ babel = ["Babel"]
361
  lingua = ["lingua"]
362
  testing = ["pytest"]
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  [[package]]
365
  name = "markupsafe"
366
  version = "3.0.2"
@@ -431,6 +627,116 @@ files = [
431
  {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
432
  ]
433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  [[package]]
435
  name = "pydantic"
436
  version = "2.11.5"
@@ -563,6 +869,54 @@ files = [
563
  [package.dependencies]
564
  typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  [[package]]
567
  name = "python-dotenv"
568
  version = "1.1.0"
@@ -577,6 +931,17 @@ files = [
577
  [package.extras]
578
  cli = ["click (>=5.0)"]
579
 
 
 
 
 
 
 
 
 
 
 
 
580
  [[package]]
581
  name = "pyyaml"
582
  version = "6.0.2"
@@ -650,6 +1015,17 @@ files = [
650
  {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
651
  ]
652
 
 
 
 
 
 
 
 
 
 
 
 
653
  [[package]]
654
  name = "sqlalchemy"
655
  version = "2.0.41"
@@ -1041,4 +1417,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
1041
  [metadata]
1042
  lock-version = "2.0"
1043
  python-versions = "^3.12"
1044
- content-hash = "8847df5b20f9482867f62a2a527c304d3bb38c4f8ca745f1b4a18fbd8481280d"
 
1
  # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
2
 
3
+ [[package]]
4
+ name = "aiofiles"
5
+ version = "24.1.0"
6
+ description = "File support for asyncio."
7
+ optional = false
8
+ python-versions = ">=3.8"
9
+ files = [
10
+ {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"},
11
+ {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
12
+ ]
13
+
14
  [[package]]
15
  name = "aiosqlite"
16
  version = "0.21.0"
 
143
  gssauth = ["gssapi", "sspilib"]
144
  test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"]
145
 
146
+ [[package]]
147
+ name = "beautifulsoup4"
148
+ version = "4.13.4"
149
+ description = "Screen-scraping library"
150
+ optional = false
151
+ python-versions = ">=3.7.0"
152
+ files = [
153
+ {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"},
154
+ {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"},
155
+ ]
156
+
157
+ [package.dependencies]
158
+ soupsieve = ">1.2"
159
+ typing-extensions = ">=4.0.0"
160
+
161
+ [package.extras]
162
+ cchardet = ["cchardet"]
163
+ chardet = ["chardet"]
164
+ charset-normalizer = ["charset-normalizer"]
165
+ html5lib = ["html5lib"]
166
+ lxml = ["lxml"]
167
+
168
  [[package]]
169
  name = "click"
170
  version = "8.2.1"
 
375
  [package.extras]
376
  dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"]
377
 
378
+ [[package]]
379
+ name = "lxml"
380
+ version = "5.4.0"
381
+ description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
382
+ optional = false
383
+ python-versions = ">=3.6"
384
+ files = [
385
+ {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"},
386
+ {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"},
387
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"},
388
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"},
389
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"},
390
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"},
391
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"},
392
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"},
393
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"},
394
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"},
395
+ {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"},
396
+ {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"},
397
+ {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"},
398
+ {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"},
399
+ {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"},
400
+ {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"},
401
+ {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"},
402
+ {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"},
403
+ {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"},
404
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"},
405
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"},
406
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"},
407
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"},
408
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"},
409
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"},
410
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"},
411
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"},
412
+ {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"},
413
+ {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"},
414
+ {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"},
415
+ {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"},
416
+ {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"},
417
+ {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"},
418
+ {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"},
419
+ {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"},
420
+ {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"},
421
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"},
422
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"},
423
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"},
424
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"},
425
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"},
426
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"},
427
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"},
428
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"},
429
+ {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"},
430
+ {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"},
431
+ {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"},
432
+ {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"},
433
+ {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"},
434
+ {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"},
435
+ {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"},
436
+ {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"},
437
+ {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"},
438
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"},
439
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"},
440
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"},
441
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"},
442
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"},
443
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"},
444
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"},
445
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"},
446
+ {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"},
447
+ {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"},
448
+ {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"},
449
+ {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"},
450
+ {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"},
451
+ {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"},
452
+ {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"},
453
+ {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"},
454
+ {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"},
455
+ {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"},
456
+ {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"},
457
+ {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"},
458
+ {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"},
459
+ {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"},
460
+ {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"},
461
+ {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"},
462
+ {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"},
463
+ {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"},
464
+ {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"},
465
+ {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"},
466
+ {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"},
467
+ {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"},
468
+ {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"},
469
+ {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"},
470
+ {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"},
471
+ {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"},
472
+ {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"},
473
+ {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"},
474
+ {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"},
475
+ {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"},
476
+ {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"},
477
+ {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"},
478
+ {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"},
479
+ {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"},
480
+ {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"},
481
+ {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"},
482
+ {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"},
483
+ {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"},
484
+ {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"},
485
+ {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"},
486
+ {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"},
487
+ {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"},
488
+ {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"},
489
+ {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"},
490
+ {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"},
491
+ {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"},
492
+ {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"},
493
+ {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"},
494
+ {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"},
495
+ {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"},
496
+ {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"},
497
+ {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"},
498
+ {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"},
499
+ {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"},
500
+ {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"},
501
+ {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"},
502
+ {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"},
503
+ {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"},
504
+ {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"},
505
+ {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"},
506
+ {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"},
507
+ {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"},
508
+ {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"},
509
+ {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"},
510
+ {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"},
511
+ {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"},
512
+ {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"},
513
+ {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"},
514
+ {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"},
515
+ {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"},
516
+ {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"},
517
+ ]
518
+
519
+ [package.extras]
520
+ cssselect = ["cssselect (>=0.7)"]
521
+ html-clean = ["lxml_html_clean"]
522
+ html5 = ["html5lib"]
523
+ htmlsoup = ["BeautifulSoup4"]
524
+ source = ["Cython (>=3.0.11,<3.1.0)"]
525
+
526
  [[package]]
527
  name = "mako"
528
  version = "1.3.10"
 
542
  lingua = ["lingua"]
543
  testing = ["pytest"]
544
 
545
+ [[package]]
546
+ name = "markdown"
547
+ version = "3.8"
548
+ description = "Python implementation of John Gruber's Markdown."
549
+ optional = false
550
+ python-versions = ">=3.9"
551
+ files = [
552
+ {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"},
553
+ {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"},
554
+ ]
555
+
556
+ [package.extras]
557
+ docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
558
+ testing = ["coverage", "pyyaml"]
559
+
560
  [[package]]
561
  name = "markupsafe"
562
  version = "3.0.2"
 
627
  {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
628
  ]
629
 
630
+ [[package]]
631
+ name = "packaging"
632
+ version = "25.0"
633
+ description = "Core utilities for Python packages"
634
+ optional = false
635
+ python-versions = ">=3.8"
636
+ files = [
637
+ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
638
+ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
639
+ ]
640
+
641
+ [[package]]
642
+ name = "pillow"
643
+ version = "11.2.1"
644
+ description = "Python Imaging Library (Fork)"
645
+ optional = false
646
+ python-versions = ">=3.9"
647
+ files = [
648
+ {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"},
649
+ {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"},
650
+ {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"},
651
+ {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"},
652
+ {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"},
653
+ {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"},
654
+ {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"},
655
+ {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"},
656
+ {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"},
657
+ {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"},
658
+ {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"},
659
+ {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"},
660
+ {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"},
661
+ {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"},
662
+ {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"},
663
+ {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"},
664
+ {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"},
665
+ {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"},
666
+ {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"},
667
+ {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"},
668
+ {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"},
669
+ {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"},
670
+ {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"},
671
+ {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"},
672
+ {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"},
673
+ {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"},
674
+ {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"},
675
+ {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"},
676
+ {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"},
677
+ {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"},
678
+ {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"},
679
+ {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"},
680
+ {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"},
681
+ {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"},
682
+ {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"},
683
+ {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"},
684
+ {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"},
685
+ {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"},
686
+ {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"},
687
+ {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"},
688
+ {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"},
689
+ {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"},
690
+ {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"},
691
+ {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"},
692
+ {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"},
693
+ {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"},
694
+ {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"},
695
+ {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"},
696
+ {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"},
697
+ {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"},
698
+ {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"},
699
+ {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"},
700
+ {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"},
701
+ {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"},
702
+ {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"},
703
+ {file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"},
704
+ {file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"},
705
+ {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"},
706
+ {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"},
707
+ {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"},
708
+ {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"},
709
+ {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"},
710
+ {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"},
711
+ {file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"},
712
+ {file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"},
713
+ {file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"},
714
+ {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"},
715
+ {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"},
716
+ {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"},
717
+ {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"},
718
+ {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"},
719
+ {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"},
720
+ {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"},
721
+ {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"},
722
+ {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"},
723
+ {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"},
724
+ {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"},
725
+ {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"},
726
+ {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"},
727
+ {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"},
728
+ {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"},
729
+ ]
730
+
731
+ [package.extras]
732
+ docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
733
+ fpx = ["olefile"]
734
+ mic = ["olefile"]
735
+ test-arrow = ["pyarrow"]
736
+ tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"]
737
+ typing = ["typing-extensions"]
738
+ xmp = ["defusedxml"]
739
+
740
  [[package]]
741
  name = "pydantic"
742
  version = "2.11.5"
 
869
  [package.dependencies]
870
  typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
871
 
872
+ [[package]]
873
+ name = "pypdf2"
874
+ version = "3.0.1"
875
+ description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files"
876
+ optional = false
877
+ python-versions = ">=3.6"
878
+ files = [
879
+ {file = "PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440"},
880
+ {file = "pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928"},
881
+ ]
882
+
883
+ [package.extras]
884
+ crypto = ["PyCryptodome"]
885
+ dev = ["black", "flit", "pip-tools", "pre-commit (<2.18.0)", "pytest-cov", "wheel"]
886
+ docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"]
887
+ full = ["Pillow", "PyCryptodome"]
888
+ image = ["Pillow"]
889
+
890
+ [[package]]
891
+ name = "pytesseract"
892
+ version = "0.3.13"
893
+ description = "Python-tesseract is a python wrapper for Google's Tesseract-OCR"
894
+ optional = false
895
+ python-versions = ">=3.8"
896
+ files = [
897
+ {file = "pytesseract-0.3.13-py3-none-any.whl", hash = "sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34"},
898
+ {file = "pytesseract-0.3.13.tar.gz", hash = "sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9"},
899
+ ]
900
+
901
+ [package.dependencies]
902
+ packaging = ">=21.3"
903
+ Pillow = ">=8.0.0"
904
+
905
+ [[package]]
906
+ name = "python-docx"
907
+ version = "1.1.2"
908
+ description = "Create, read, and update Microsoft Word .docx files."
909
+ optional = false
910
+ python-versions = ">=3.7"
911
+ files = [
912
+ {file = "python_docx-1.1.2-py3-none-any.whl", hash = "sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe"},
913
+ {file = "python_docx-1.1.2.tar.gz", hash = "sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd"},
914
+ ]
915
+
916
+ [package.dependencies]
917
+ lxml = ">=3.1.0"
918
+ typing-extensions = ">=4.9.0"
919
+
920
  [[package]]
921
  name = "python-dotenv"
922
  version = "1.1.0"
 
931
  [package.extras]
932
  cli = ["click (>=5.0)"]
933
 
934
+ [[package]]
935
+ name = "python-multipart"
936
+ version = "0.0.20"
937
+ description = "A streaming multipart parser for Python"
938
+ optional = false
939
+ python-versions = ">=3.8"
940
+ files = [
941
+ {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
942
+ {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
943
+ ]
944
+
945
  [[package]]
946
  name = "pyyaml"
947
  version = "6.0.2"
 
1015
  {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
1016
  ]
1017
 
1018
+ [[package]]
1019
+ name = "soupsieve"
1020
+ version = "2.7"
1021
+ description = "A modern CSS selector implementation for Beautiful Soup."
1022
+ optional = false
1023
+ python-versions = ">=3.8"
1024
+ files = [
1025
+ {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"},
1026
+ {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"},
1027
+ ]
1028
+
1029
  [[package]]
1030
  name = "sqlalchemy"
1031
  version = "2.0.41"
 
1417
  [metadata]
1418
  lock-version = "2.0"
1419
  python-versions = "^3.12"
1420
+ content-hash = "c70a34fe44dd8f6e484e18f5c9ab595121a4477e3b18b9db9849a040a9f4d3c1"
pyproject.toml CHANGED
@@ -15,6 +15,14 @@ asyncpg = "^0.30.0"
15
  loguru = "^0.7.3"
16
  pydantic = "^2.11.5"
17
  aiosqlite = "^0.21.0"
 
 
 
 
 
 
 
 
18
 
19
 
20
  [build-system]
 
15
  loguru = "^0.7.3"
16
  pydantic = "^2.11.5"
17
  aiosqlite = "^0.21.0"
18
+ markdown = "^3.8"
19
+ pypdf2 = "^3.0.1"
20
+ pillow = "^11.2.1"
21
+ aiofiles = "^24.1.0"
22
+ pytesseract = "^0.3.13"
23
+ python-docx = "^1.1.2"
24
+ python-multipart = "^0.0.20"
25
+ beautifulsoup4 = "^4.13.4"
26
 
27
 
28
  [build-system]
src/controllers/__init__.py CHANGED
@@ -1,8 +1,10 @@
1
  from fastapi import APIRouter
2
  from ._rfp_controller import RFPController
 
3
 
4
  api_router = APIRouter()
5
  api_router.include_router(RFPController().router)
 
6
 
7
  __all__ = ["api_router"]
8
  __version__ = "0.1.0"
 
1
  from fastapi import APIRouter
2
  from ._rfp_controller import RFPController
3
+ from ._sow_controller import SOWController
4
 
5
  api_router = APIRouter()
6
  api_router.include_router(RFPController().router)
7
+ api_router.include_router(SOWController().router)
8
 
9
  __all__ = ["api_router"]
10
  __version__ = "0.1.0"
src/controllers/_sow_controller.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import File, Path, Response, UploadFile
2
+ from fastapi.responses import FileResponse
3
+ from fastapi import APIRouter, HTTPException, Query
4
+ from pydantic import BaseModel, Field, field_validator
5
+ from typing import List, Optional
6
+ from uuid import UUID
7
+
8
+ from src.config import logger
9
+ from src.services import SOWService
10
+ from datetime import datetime
11
+
12
+ from src.models import SOW
13
+
14
+
15
+ class SOW(BaseModel):
16
+ id: UUID
17
+ rfp_id: UUID
18
+ requirement: Optional[str] = None
19
+ additional_info: Optional[str] = None
20
+ sow_generated_text: Optional[str] = None
21
+ created_at: datetime
22
+ updated_at: datetime
23
+
24
+
25
+ class ResponseSOW(BaseModel):
26
+ status: str
27
+ data: Optional[List[SOW]]
28
+
29
+
30
+ class ResponseSOWSingle(BaseModel):
31
+ status: str
32
+ data: SOW
33
+
34
+
35
+ class SOWRequest(BaseModel):
36
+ requirement: Optional[str] = None
37
+ additional_info: Optional[str] = None
38
+ sow_generated_text: Optional[str] = None
39
+
40
+
41
+ class SOWUpdateRequest(BaseModel):
42
+ requirement: Optional[str] = None
43
+ additional_info: Optional[str] = None
44
+ sow_generated_text: Optional[str] = None
45
+
46
+
47
+ class DeleteResponse(BaseModel):
48
+ status: str
49
+
50
+
51
+ class SOWController:
52
+ def __init__(self):
53
+ self.__sow_service = SOWService
54
+ self.router = APIRouter()
55
+ self.router.add_api_route(
56
+ "/rfps/{rfp_id}/sows",
57
+ self.get_sows,
58
+ methods=["GET"],
59
+ response_model=ResponseSOW,
60
+ tags=["SOWs by RFP ID"],
61
+ )
62
+ self.router.add_api_route(
63
+ "/rfps/{rfp_id}/sows/{sow_id}",
64
+ self.get_sows_by_id,
65
+ methods=["GET"],
66
+ response_model=ResponseSOWSingle,
67
+ tags=["SOWs by RFP ID and SOW ID"],
68
+ )
69
+ self.router.add_api_route(
70
+ "/rfps/{rfp_id}/sows",
71
+ self.create_sow,
72
+ methods=["POST"],
73
+ response_model=ResponseSOWSingle,
74
+ tags=["SOWs by RFP ID"],
75
+ )
76
+ self.router.add_api_route(
77
+ "/rfps/{rfp_id}/sows/{sow_id}",
78
+ self.update_sow,
79
+ methods=["PATCH"],
80
+ response_model=ResponseSOWSingle,
81
+ tags=["SOWs by RFP ID and SOW ID"],
82
+ )
83
+ self.router.add_api_route(
84
+ "/rfps/{rfp_id}/sows/{sow_id}",
85
+ self.delete_sow,
86
+ methods=["DELETE"],
87
+ response_model=DeleteResponse,
88
+ tags=["SOWs by RFP ID and SOW ID"],
89
+ )
90
+
91
+ self.router.add_api_route(
92
+ "/rfps/{rfp_id}/sows/{sow_id}/generate",
93
+ self.generate_sow,
94
+ methods=["POST"],
95
+ response_model=ResponseSOWSingle,
96
+ tags=["Generate SOW"],
97
+ )
98
+
99
+ self.router.add_api_route(
100
+ "/rfps/{rfp_id}/sows/{sow_id}/download",
101
+ self.download_sow,
102
+ methods=["GET"],
103
+ response_model=None,
104
+ tags=["Download SOW"],
105
+ response_class=FileResponse,
106
+ )
107
+
108
+ self.router.add_api_route(
109
+ "/rfps/{rfp_id}/sows/{sow_id}/upload",
110
+ self.upload_sow,
111
+ methods=["POST"],
112
+ response_model=ResponseSOWSingle,
113
+ tags=["Upload Requirement File"],
114
+ )
115
+
116
+ async def get_sows(self, rfp_id: UUID = Path(...)):
117
+ async with self.__sow_service() as service:
118
+ try:
119
+ sows = await service.get_sows(rfp_id=rfp_id)
120
+ return ResponseSOW(status="success", data=[SOW(**sow) for sow in sows])
121
+ except HTTPException as e:
122
+ logger.warning(e)
123
+ raise e
124
+ except Exception as e:
125
+ logger.exception(e)
126
+ raise HTTPException(status_code=500, detail="Failed to retrieve SOWs.")
127
+
128
+ async def get_sows_by_id(self, rfp_id: UUID = Path(...), sow_id: UUID = Path(...)):
129
+ async with self.__sow_service() as service:
130
+ try:
131
+ sows = await service.get_sows(rfp_id=rfp_id, sow_id=sow_id)
132
+ sow = sows[0]
133
+ return ResponseSOWSingle(status="success", data=SOW(**sow))
134
+ except HTTPException as e:
135
+ logger.warning(e)
136
+ raise e
137
+ except Exception as e:
138
+ logger.exception(e)
139
+ raise HTTPException(status_code=500, detail="Failed to retrieve SOWs.")
140
+
141
+ async def create_sow(self, sow: SOWRequest, rfp_id: UUID = Path(...)):
142
+ async with self.__sow_service() as service:
143
+ try:
144
+ sow = await service.create_sow(
145
+ rfp_id=rfp_id, sow=sow.model_dump(mode="json", exclude_unset=True)
146
+ )
147
+ return ResponseSOWSingle(status="success", data=SOW(**sow))
148
+ except HTTPException as e:
149
+ logger.warning(e)
150
+ raise e
151
+ except Exception as e:
152
+ logger.exception(e)
153
+ raise HTTPException(status_code=500, detail="Failed to create SOW.")
154
+
155
+ async def update_sow(
156
+ self, sow: SOWUpdateRequest, rfp_id: UUID = Path(...), sow_id: UUID = Path(...)
157
+ ):
158
+ async with self.__sow_service() as service:
159
+ try:
160
+ sow = await service.update_sow(
161
+ rfp_id=rfp_id,
162
+ sow_id=sow_id,
163
+ sow=sow.model_dump(mode="json", exclude_unset=True),
164
+ )
165
+ return ResponseSOWSingle(status="success", data=SOW(**sow))
166
+ except HTTPException as e:
167
+ logger.warning(e)
168
+ raise e
169
+ except Exception as e:
170
+ logger.exception(e)
171
+ raise HTTPException(status_code=500, detail="Failed to update SOW.")
172
+
173
+ async def delete_sow(self, rfp_id: UUID = Path(...), sow_id: UUID = Path(...)):
174
+ async with self.__sow_service() as service:
175
+ try:
176
+ status = await service.delete_sow(rfp_id=rfp_id, sow_id=sow_id)
177
+ if not status:
178
+ raise HTTPException(status_code=404, detail="SOW not found.")
179
+ return DeleteResponse(status="success")
180
+ except HTTPException as e:
181
+ logger.warning(e)
182
+ raise e
183
+ except Exception as e:
184
+ logger.exception(e)
185
+ raise HTTPException(status_code=500, detail="Failed to delete SOW.")
186
+
187
+ async def generate_sow(self, rfp_id: UUID = Path(...), sow_id: UUID = Path(...)):
188
+ async with self.__sow_service() as service:
189
+ try:
190
+ sow = await service.generate_sow(rfp_id=rfp_id, sow_id=sow_id)
191
+ return ResponseSOWSingle(status="success", data=SOW(**sow))
192
+ except HTTPException as e:
193
+ logger.warning(e)
194
+ raise e
195
+ except Exception as e:
196
+ logger.exception(e)
197
+ raise HTTPException(status_code=500, detail="Failed to generate SOW.")
198
+
199
+ async def download_sow(self, rfp_id: UUID = Path(...), sow_id: UUID = Path(...)):
200
+ async with self.__sow_service() as service:
201
+ try:
202
+ file = await service.download_sow(rfp_id=rfp_id, sow_id=sow_id)
203
+ return Response(
204
+ content=file,
205
+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
206
+ )
207
+ except HTTPException as e:
208
+ logger.warning(e)
209
+ raise e
210
+ except Exception as e:
211
+ logger.exception(e)
212
+ raise HTTPException(status_code=500, detail="Failed to download SOW.")
213
+
214
+ async def upload_sow(
215
+ self,
216
+ rfp_id: UUID = Path(...),
217
+ sow_id: UUID = Path(...),
218
+ file: UploadFile = File(...),
219
+ ):
220
+ async with self.__sow_service() as service:
221
+ try:
222
+ data = await service.upload_sow(rfp_id=rfp_id, sow_id=sow_id, file=file)
223
+ return ResponseSOWSingle(status="success", data=SOW(**data))
224
+ except HTTPException as e:
225
+ logger.warning(e)
226
+ raise e
227
+ except Exception as e:
228
+ logger.exception(e)
229
+ raise HTTPException(status_code=500, detail="Failed to upload SOW.")
src/models/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
  from ._base import Base
2
  from ._rfp import RFP, RFPStatus
 
3
 
4
- __all__ = ["Base", "RFP", "RFPStatus"]
5
  __version__ = "0.1.0"
6
- __author__ = "Aryan Jain"
 
1
  from ._base import Base
2
  from ._rfp import RFP, RFPStatus
3
+ from ._sows import SOW
4
 
5
+ __all__ = ["Base", "RFP", "RFPStatus", "SOW"]
6
  __version__ = "0.1.0"
7
+ __author__ = "Aryan Jain"
src/models/_sows.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum as PyEnum
2
+
3
+ from sqlalchemy import (
4
+ Boolean,
5
+ Column,
6
+ DateTime,
7
+ Enum,
8
+ Float,
9
+ ForeignKey,
10
+ Integer,
11
+ String,
12
+ func,
13
+ )
14
+ from sqlalchemy.dialects.postgresql import UUID
15
+ from pydantic import BaseModel
16
+ from ._base import Base
17
+
18
+
19
+ class SOW(Base):
20
+ __tablename__ = "sows"
21
+ id = Column(UUID(as_uuid=True), primary_key=True, nullable=False)
22
+ rfp_id = Column(
23
+ UUID(as_uuid=True),
24
+ ForeignKey("rfps.id", ondelete="CASCADE"),
25
+ nullable=False,
26
+ unique=True,
27
+ )
28
+ requirement = Column(String, nullable=True)
29
+ additional_info = Column(String, nullable=True)
30
+ sow_generated_text = Column(String, nullable=True)
31
+ created_at = Column(DateTime, nullable=False, default=func.now())
32
+ updated_at = Column(
33
+ DateTime, nullable=False, default=func.now(), onupdate=func.now()
34
+ )
src/repositories/__init__.py CHANGED
@@ -1,6 +1,11 @@
1
  from ._base_repository import BaseRepository
2
  from ._rfp_repository import RFPRepository
 
3
 
4
- __all__ = ["BaseRepository", "RFPRepository"]
 
 
 
 
5
  __version__ = "0.1.0"
6
  __author__ = "Aryan Jain"
 
1
  from ._base_repository import BaseRepository
2
  from ._rfp_repository import RFPRepository
3
+ from ._sow_repository import SOWRepository
4
 
5
+ __all__ = [
6
+ "BaseRepository",
7
+ "RFPRepository",
8
+ "SOWRepository"
9
+ ]
10
  __version__ = "0.1.0"
11
  __author__ = "Aryan Jain"
src/repositories/_sow_repository.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from sqlalchemy import select
3
+ from src.models import SOW
4
+ from ._base_repository import BaseRepository
5
+
6
+
7
+ class SOWRepository(BaseRepository):
8
+ def __init__(self):
9
+ super().__init__(model=SOW)
10
+
11
+ async def __aenter__(self):
12
+ return self
13
+
14
+ async def __aexit__(self, exc_type, exc_value, traceback):
15
+ pass
16
+
17
+ async def get_sows(self, rfp_id: uuid.UUID = None, sow_id: uuid.UUID = None):
18
+ async with self.get_session() as session:
19
+ query = select(SOW)
20
+ if rfp_id:
21
+ query = query.where(SOW.rfp_id == rfp_id)
22
+ if sow_id:
23
+ query = query.where(SOW.id == sow_id)
24
+ result = await session.execute(query)
25
+ final_results = result.scalars().all()
26
+ result_dict = [
27
+ {
28
+ k: v
29
+ for k, v in final_result.__dict__.items()
30
+ if not k.startswith("_")
31
+ }
32
+ for final_result in final_results
33
+ ]
34
+ return result_dict
35
+
36
+ async def create_sow(self, sow: dict):
37
+ sow["id"] = uuid.uuid4()
38
+ async with self.get_session() as session:
39
+ instance = SOW(**sow)
40
+ session.add(instance)
41
+ await session.commit()
42
+ await session.refresh(instance)
43
+ result_dict = {
44
+ k: v for k, v in instance.__dict__.items() if not k.startswith("_")
45
+ }
46
+ return result_dict
47
+
48
+ async def update_sow(self, rfp_id: uuid.UUID, sow_id: uuid.UUID, sow: dict):
49
+ async with self.get_session() as session:
50
+ instance = await session.get(SOW, sow_id)
51
+ if not instance or instance.rfp_id != rfp_id:
52
+ return None
53
+ for key, value in sow.items():
54
+ setattr(instance, key, value)
55
+ await session.commit()
56
+ await session.refresh(instance)
57
+ result_dict = {
58
+ k: v for k, v in instance.__dict__.items() if not k.startswith("_")
59
+ }
60
+ return result_dict
61
+
62
+ async def delete_sow(self, rfp_id: uuid.UUID, sow_id: uuid.UUID):
63
+ async with self.get_session() as session:
64
+ instance = await session.get(SOW, sow_id)
65
+ if not instance or instance.rfp_id != rfp_id:
66
+ return False
67
+ await session.delete(instance)
68
+ await session.commit()
69
+ return True
src/services/__init__.py CHANGED
@@ -1,5 +1,9 @@
1
  from ._rfp_service import RFPService
 
2
 
3
- __all__ = ["RFPService"]
 
 
 
4
  __version__ = "0.1.0"
5
  __author__ = "Aryan Jain"
 
1
  from ._rfp_service import RFPService
2
+ from ._sow_service import SOWService
3
 
4
+ __all__ = [
5
+ "RFPService",
6
+ "SOWService"
7
+ ]
8
  __version__ = "0.1.0"
9
  __author__ = "Aryan Jain"
src/services/_sow_service.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException
2
+ from typing import List, Optional
3
+ from uuid import UUID
4
+ from datetime import datetime
5
+ from fastapi import UploadFile
6
+
7
+ from src.config import logger
8
+ from src.models import SOW
9
+ from src.repositories import SOWRepository
10
+ from src.utils import SOWClient
11
+
12
+
13
+ class SOWService:
14
+ def __init__(self):
15
+ self.__repository = SOWRepository
16
+ self.__client = SOWClient
17
+
18
+ async def __aenter__(self):
19
+ return self
20
+
21
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
22
+ pass
23
+
24
+ async def get_sows(self, rfp_id: UUID, sow_id: Optional[UUID] = None) -> List[dict]:
25
+ try:
26
+ async with self.__repository() as repository:
27
+ return await repository.get_sows(rfp_id=rfp_id, sow_id=sow_id)
28
+ except Exception as e:
29
+ logger.exception(e)
30
+ raise HTTPException(
31
+ status_code=500, detail="Database error while fetching SOWs"
32
+ )
33
+
34
+ async def create_sow(self, rfp_id: UUID, sow: dict) -> dict:
35
+ try:
36
+ sow["rfp_id"] = rfp_id
37
+ async with self.__repository() as repository:
38
+ return await repository.create_sow(sow)
39
+ except Exception as e:
40
+ logger.exception(e)
41
+ raise HTTPException(
42
+ status_code=500, detail="Database error while creating SOW"
43
+ )
44
+
45
+ async def update_sow(self, rfp_id: UUID, sow_id: UUID, sow: dict) -> dict:
46
+ try:
47
+ async with self.__repository() as repository:
48
+ result = await repository.update_sow(
49
+ rfp_id=rfp_id, sow_id=sow_id, sow=sow
50
+ )
51
+ if not result:
52
+ raise HTTPException(status_code=404, detail="SOW not found")
53
+ return result
54
+ except HTTPException:
55
+ raise
56
+ except Exception as e:
57
+ logger.exception(e)
58
+ raise HTTPException(
59
+ status_code=500, detail="Database error while updating SOW"
60
+ )
61
+
62
+ async def delete_sow(self, rfp_id: UUID, sow_id: UUID) -> bool:
63
+ try:
64
+ async with self.__repository() as repository:
65
+ return await repository.delete_sow(rfp_id=rfp_id, sow_id=sow_id)
66
+ except Exception as e:
67
+ logger.exception(e)
68
+ raise HTTPException(
69
+ status_code=500, detail="Database error while deleting SOW"
70
+ )
71
+
72
+ async def generate_sow(self, rfp_id: UUID, sow_id: UUID) -> dict:
73
+ pass
74
+
75
+ async def download_sow(self, rfp_id: UUID, sow_id: UUID) -> bytes:
76
+ try:
77
+ sow_data = await self.get_sows(rfp_id, sow_id)
78
+ if not sow_data:
79
+ raise HTTPException(status_code=404, detail="SOW not found")
80
+
81
+ sow = sow_data[0]
82
+
83
+ markdown_text = sow.get("sow_generated_text")
84
+ async with self.__client() as client:
85
+ return await client.markdown_to_docx(markdown_text)
86
+ except HTTPException:
87
+ raise
88
+ except Exception as e:
89
+ logger.exception(e)
90
+ raise HTTPException(status_code=500, detail="Error while downloading SOW")
91
+
92
+ async def upload_sow(self, rfp_id: UUID, sow_id: UUID, file: UploadFile) -> dict:
93
+ try:
94
+ async with self.__client() as client:
95
+ extracted_text = await client.extract_text_from_file(file=file)
96
+
97
+ updated_sow = await self.update_sow(
98
+ rfp_id=rfp_id, sow_id=sow_id, sow={"requirement": extracted_text}
99
+ )
100
+
101
+ if not updated_sow:
102
+ raise HTTPException(status_code=404, detail="SOW not found")
103
+
104
+ return updated_sow
105
+ except HTTPException:
106
+ raise
107
+ except Exception as e:
108
+ logger.exception(e)
109
+ raise HTTPException(status_code=500, detail="Error while uploading SOW")
src/utils/__init__.py CHANGED
@@ -1,2 +1,5 @@
 
 
 
1
  __version__ = "0.1.0"
2
  __author__ = "Aryan Jain"
 
1
+ from ._sow_client import SOWClient
2
+
3
+ __all__ = ["SOWClient"]
4
  __version__ = "0.1.0"
5
  __author__ = "Aryan Jain"
src/utils/_sow_client.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ from typing import BinaryIO
4
+ from fastapi import UploadFile
5
+ import markdown
6
+ from docx import Document
7
+ from docx.shared import Inches
8
+ from bs4 import BeautifulSoup
9
+ import PyPDF2
10
+ from PIL import Image
11
+ import pytesseract
12
+ import aiofiles
13
+ import asyncio
14
+ from src.config import logger
15
+
16
+
17
+ class SOWClient:
18
+ def __init__(self):
19
+ tesseract_path = os.getenv("TESSERACT_PATH", "/usr/bin/tesseract")
20
+ if os.path.exists(tesseract_path):
21
+ pytesseract.pytesseract.tesseract_cmd = tesseract_path
22
+ logger.info(f"Tesseract path configured: {tesseract_path}")
23
+ else:
24
+ logger.warning(
25
+ f"Tesseract not found at {tesseract_path}. OCR functionality may not work."
26
+ )
27
+
28
+ async def __aenter__(self):
29
+ return self
30
+
31
+ async def __aexit__(self, exc_type, exc_value, traceback):
32
+ pass
33
+
34
+ async def extract_text_from_file(self, file: UploadFile) -> str:
35
+ """Extract text from various file formats."""
36
+ content = await file.read()
37
+ file_extension = os.path.splitext(file.filename)[1].lower()
38
+
39
+ if file_extension in [".jpg", ".jpeg", ".png"]:
40
+ return await self._extract_text_from_image(content)
41
+ elif file_extension == ".pdf":
42
+ return await self._extract_text_from_pdf(content)
43
+ elif file_extension == ".docx":
44
+ return await self._extract_text_from_docx(content)
45
+ elif file_extension in [".txt", ".md"]:
46
+ return content.decode("utf-8")
47
+ else:
48
+ raise ValueError(f"Unsupported file format: {file_extension}")
49
+
50
+ async def _extract_text_from_image(self, content: bytes) -> str:
51
+ """Extract text from image using OCR."""
52
+ try:
53
+ temp_dir = os.path.join(os.getcwd(), "temp")
54
+ os.makedirs(temp_dir, exist_ok=True)
55
+ temp_path = os.path.join(
56
+ temp_dir, f"temp_{asyncio.get_event_loop().time()}.png"
57
+ )
58
+
59
+ async with aiofiles.open(temp_path, "wb") as temp_file:
60
+ await temp_file.write(content)
61
+
62
+ loop = asyncio.get_event_loop()
63
+ image = Image.open(temp_path)
64
+ text = await loop.run_in_executor(None, pytesseract.image_to_string, image)
65
+
66
+ os.remove(temp_path)
67
+ return text
68
+ except Exception as e:
69
+ logger.error(f"Error extracting text from image: {str(e)}")
70
+ raise ValueError(
71
+ "Failed to extract text from image. Please ensure Tesseract is properly installed."
72
+ )
73
+
74
+ async def _extract_text_from_pdf(self, content: bytes) -> str:
75
+ """Extract text from PDF."""
76
+ try:
77
+ pdf_file = io.BytesIO(content)
78
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
79
+ text = ""
80
+ for page in pdf_reader.pages:
81
+ text += page.extract_text()
82
+ return text
83
+ except Exception as e:
84
+ logger.error(f"Error extracting text from PDF: {str(e)}")
85
+ raise ValueError("Failed to extract text from PDF")
86
+
87
+ async def _extract_text_from_docx(self, content: bytes) -> str:
88
+ """Extract text from DOCX."""
89
+ try:
90
+ docx_file = io.BytesIO(content)
91
+ doc = Document(docx_file)
92
+ text = ""
93
+ for paragraph in doc.paragraphs:
94
+ text += paragraph.text + "\n"
95
+ return text
96
+ except Exception as e:
97
+ logger.error(f"Error extracting text from DOCX: {str(e)}")
98
+ raise ValueError("Failed to extract text from DOCX")
99
+
100
+ async def markdown_to_docx(self, markdown_text: str) -> bytes:
101
+ """Convert markdown text to DOCX format."""
102
+ try:
103
+ html = markdown.markdown(markdown_text)
104
+ soup = BeautifulSoup(html, "html.parser")
105
+
106
+ doc = Document()
107
+ for element in soup.contents:
108
+ if element.name == "h1":
109
+ doc.add_heading(element.text, level=1)
110
+ elif element.name == "h2":
111
+ doc.add_heading(element.text, level=2)
112
+ elif element.name == "h3":
113
+ doc.add_heading(element.text, level=3)
114
+ elif element.name == "p":
115
+ doc.add_paragraph(element.text)
116
+ elif element.name == "ul":
117
+ for li in element.find_all("li"):
118
+ doc.add_paragraph(li.text, style="List Bullet")
119
+ elif element.name == "ol":
120
+ for li in element.find_all("li"):
121
+ doc.add_paragraph(li.text, style="List Number")
122
+ elif element.name == "blockquote":
123
+ doc.add_paragraph(element.text, style="Intense Quote")
124
+ else:
125
+ doc.add_paragraph(element.text)
126
+
127
+ docx_bytes = io.BytesIO()
128
+ doc.save(docx_bytes)
129
+ docx_bytes.seek(0)
130
+
131
+ return docx_bytes.getvalue()
132
+ except Exception as e:
133
+ logger.error(f"Error converting markdown to DOCX: {str(e)}")
134
+ raise ValueError("Failed to convert markdown to DOCX")