Soumik-404 commited on
Commit
89157f5
·
1 Parent(s): 19bd230

feat: add db apis

Browse files
.gitignore CHANGED
@@ -24,6 +24,8 @@ share/python-wheels/
24
  *.egg
25
  MANIFEST
26
 
 
 
27
  *.manifest
28
  *.spec
29
 
@@ -117,3 +119,5 @@ logs/
117
 
118
  *.tmp
119
  *.temp
 
 
 
24
  *.egg
25
  MANIFEST
26
 
27
+ *collection.json
28
+
29
  *.manifest
30
  *.spec
31
 
 
119
 
120
  *.tmp
121
  *.temp
122
+
123
+ local_deploy.py
Dockerfile CHANGED
@@ -1,7 +1,7 @@
1
  FROM python:3.12-slim
2
 
3
- LABEL maintainer="reconciliation-non-ai-extractor"
4
- LABEL description="Enterprise document extraction API with Microsoft MarkItDown, RapidOCR, and spaCy"
5
  LABEL version="1.0.0"
6
 
7
  RUN apt-get update && apt-get install -y --no-install-recommends \
 
1
  FROM python:3.12-slim
2
 
3
+ LABEL maintainer="All API Collection"
4
+ LABEL description="All API Collection - Document extraction, conversion, and database query API"
5
  LABEL version="1.0.0"
6
 
7
  RUN apt-get update && apt-get install -y --no-install-recommends \
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: MarkItDown API — v2.1.0
3
  emoji: ⚡
4
  colorFrom: red
5
  colorTo: purple
 
1
  ---
2
+ title: All API Collection — v2.1.0
3
  emoji: ⚡
4
  colorFrom: red
5
  colorTo: purple
__init__.py DELETED
@@ -1,4 +0,0 @@
1
- from __future__ import annotations
2
-
3
- __version__ = "1.0.0"
4
- __app_name__ = "reconciliation-non-ai-extractor"
 
 
 
 
 
app/api/deps.py CHANGED
@@ -5,6 +5,7 @@ from fastapi import Depends
5
  from app.core.security import require_api_key
6
  from app.services.auth_service import AuthService
7
  from app.services.converter_service import ConverterService
 
8
  from app.services.extraction_service import ExtractionService
9
  from app.services.ocr_service import OCRService
10
 
@@ -25,5 +26,9 @@ def get_extraction_service() -> ExtractionService:
25
  return ExtractionService()
26
 
27
 
 
 
 
 
28
  def require_auth(token: str = Depends(require_api_key)) -> str:
29
  return token
 
5
  from app.core.security import require_api_key
6
  from app.services.auth_service import AuthService
7
  from app.services.converter_service import ConverterService
8
+ from app.services.database_service import DatabaseService
9
  from app.services.extraction_service import ExtractionService
10
  from app.services.ocr_service import OCRService
11
 
 
26
  return ExtractionService()
27
 
28
 
29
+ def get_database_service() -> DatabaseService:
30
+ return DatabaseService()
31
+
32
+
33
  def require_auth(token: str = Depends(require_api_key)) -> str:
34
  return token
app/api/server.py CHANGED
@@ -8,6 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware
8
  from fastapi.middleware.gzip import GZipMiddleware
9
 
10
  from app.config import get_settings
 
11
  from app.core.logger import get_logger
12
  from app.api.v1.router import api_v1_router
13
 
@@ -35,12 +36,14 @@ async def _self_ping():
35
  async def lifespan(app: FastAPI):
36
  asyncio.create_task(_self_ping())
37
  yield
 
 
38
 
39
 
40
  def create_application() -> FastAPI:
41
  app = FastAPI(
42
  title=_settings.app_name,
43
- description="Document-to-Markdown and structured data extraction API powered by Microsoft MarkItDown, RapidOCR, and spaCy.",
44
  version=_settings.app_version,
45
  docs_url="/docs",
46
  redoc_url="/redoc",
@@ -72,7 +75,7 @@ def create_application() -> FastAPI:
72
 
73
  @app.get("/ping", include_in_schema=False)
74
  async def ping():
75
- return {"message": f"{_settings.app_name} is running..."}
76
 
77
  return app
78
 
 
8
  from fastapi.middleware.gzip import GZipMiddleware
9
 
10
  from app.config import get_settings
11
+ from app.core.database import pool_manager
12
  from app.core.logger import get_logger
13
  from app.api.v1.router import api_v1_router
14
 
 
36
  async def lifespan(app: FastAPI):
37
  asyncio.create_task(_self_ping())
38
  yield
39
+ _logger.info("Shutting down database connection pools...")
40
+ await pool_manager.close_all()
41
 
42
 
43
  def create_application() -> FastAPI:
44
  app = FastAPI(
45
  title=_settings.app_name,
46
+ description="All API Collection - Document extraction, conversion, and database query API.",
47
  version=_settings.app_version,
48
  docs_url="/docs",
49
  redoc_url="/redoc",
 
75
 
76
  @app.get("/ping", include_in_schema=False)
77
  async def ping():
78
+ return {"name": f"{_settings.app_name}", "version": _settings.app_version}
79
 
80
  return app
81
 
app/api/v1/database.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, status
6
+
7
+ from app.api.deps import require_auth
8
+ from app.core.logger import get_logger
9
+ from app.models.schemas import DatabaseQueryRequest, DatabaseQueryResponse
10
+ from app.services.database_service import DatabaseService
11
+
12
+ router = APIRouter()
13
+ _logger = get_logger(__name__)
14
+
15
+
16
+ @router.post(
17
+ "/database/query",
18
+ response_model=DatabaseQueryResponse,
19
+ summary="Execute queries against MySQL, PostgreSQL, or MongoDB",
20
+ )
21
+ async def execute_database_query(
22
+ body: DatabaseQueryRequest,
23
+ token: Annotated[str, Depends(require_auth)],
24
+ db_service: Annotated[DatabaseService, Depends()] = None,
25
+ ) -> DatabaseQueryResponse:
26
+ if db_service is None:
27
+ db_service = DatabaseService()
28
+
29
+ _logger.info(
30
+ "Database query request: type=%s, %s",
31
+ body.db_type,
32
+ body.connection.safe_repr(),
33
+ )
34
+
35
+ try:
36
+ return await db_service.execute_query(body)
37
+ except HTTPException:
38
+ raise
39
+ except Exception as exc:
40
+ _logger.error("Unexpected error processing database query: %s", exc)
41
+ raise HTTPException(
42
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
43
+ detail={
44
+ "success": False,
45
+ "execution_time_ms": 0,
46
+ "error": {"message": f"Internal error: {exc}", "code": "INTERNAL_ERROR"},
47
+ },
48
+ )
app/api/v1/router.py CHANGED
@@ -2,9 +2,10 @@ from __future__ import annotations
2
 
3
  from fastapi import APIRouter
4
 
5
- from app.api.v1 import batch, convert, system
6
 
7
  api_v1_router = APIRouter()
8
  api_v1_router.include_router(convert.router, tags=["Convert"])
9
  api_v1_router.include_router(batch.router, tags=["Batch"])
10
  api_v1_router.include_router(system.router, tags=["System"])
 
 
2
 
3
  from fastapi import APIRouter
4
 
5
+ from app.api.v1 import batch, convert, database, system
6
 
7
  api_v1_router = APIRouter()
8
  api_v1_router.include_router(convert.router, tags=["Convert"])
9
  api_v1_router.include_router(batch.router, tags=["Batch"])
10
  api_v1_router.include_router(system.router, tags=["System"])
11
+ api_v1_router.include_router(database.router, tags=["Database"])
app/config.py CHANGED
@@ -13,7 +13,7 @@ class Settings(BaseSettings):
13
  extra="ignore",
14
  )
15
 
16
- app_name: str = "MarkItDown API"
17
  app_version: str = "1.0.0"
18
  environment: str = "production"
19
  host: str = "0.0.0.0"
 
13
  extra="ignore",
14
  )
15
 
16
+ app_name: str = "All API Collection"
17
  app_version: str = "1.0.0"
18
  environment: str = "production"
19
  host: str = "0.0.0.0"
app/core/database/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.core.database.base import BaseExecutor, ConnectionConfig, StatementResult
2
+ from app.core.database.mysql import MySQLExecutor
3
+ from app.core.database.postgresql import PostgreSQLExecutor
4
+ from app.core.database.mongodb import MongoDBExecutor
5
+ from app.core.database.pool import pool_manager
6
+
7
+ __all__ = [
8
+ "BaseExecutor",
9
+ "ConnectionConfig",
10
+ "StatementResult",
11
+ "MySQLExecutor",
12
+ "PostgreSQLExecutor",
13
+ "MongoDBExecutor",
14
+ "pool_manager",
15
+ ]
app/core/database/base.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from app.core.logger import get_logger
10
+
11
+ _logger = get_logger(__name__)
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ConnectionConfig:
16
+ db_type: str
17
+ host: str
18
+ port: int
19
+ database: str
20
+ username: str
21
+ password: str
22
+ ssl_enabled: bool = False
23
+ ssl_ca_cert: str | None = None
24
+ ssl_cert: str | None = None
25
+ ssl_key: str | None = None
26
+ query_timeout_seconds: float = 30.0
27
+ connection_timeout_seconds: float = 10.0
28
+ max_rows: int = 10000
29
+
30
+ @property
31
+ def pool_key(self) -> str:
32
+ return f"{self.db_type}:{self.username}@{self.host}:{self.port}/{self.database}"
33
+
34
+ @property
35
+ def safe_repr(self) -> str:
36
+ return (
37
+ f"ConnectionConfig(db_type={self.db_type}, host={self.host}, "
38
+ f"port={self.port}, database={self.database}, "
39
+ f"username={self.username}, ssl={self.ssl_enabled})"
40
+ )
41
+
42
+
43
+ @dataclass
44
+ class StatementResult:
45
+ success: bool
46
+ rows: int = 0
47
+ data: list[dict[str, Any]] = field(default_factory=list)
48
+ error: str | None = None
49
+ error_code: str | None = None
50
+
51
+
52
+ class BaseExecutor(ABC):
53
+ def __init__(self, config: ConnectionConfig) -> None:
54
+ self._config = config
55
+ self._pool: Any = None
56
+ self._closed = False
57
+ self._lock = asyncio.Lock()
58
+ self._max_connection_retries = 3
59
+
60
+ @abstractmethod
61
+ async def _create_pool(self) -> Any:
62
+ ...
63
+
64
+ @abstractmethod
65
+ async def _execute_queries(
66
+ self, pool: Any, queries: list[Any], use_transaction: bool
67
+ ) -> list[StatementResult]:
68
+ ...
69
+
70
+ async def execute(
71
+ self, queries: list[Any], use_transaction: bool = True
72
+ ) -> list[StatementResult]:
73
+ if self._closed:
74
+ raise RuntimeError("Executor has been closed")
75
+ pool = await self._get_or_create_pool()
76
+ return await self._execute_queries(pool, queries, use_transaction)
77
+
78
+ async def _get_or_create_pool(self) -> Any:
79
+ async with self._lock:
80
+ if self._pool is not None:
81
+ return self._pool
82
+
83
+ last_exc: Exception | None = None
84
+ for attempt in range(self._max_connection_retries):
85
+ try:
86
+ self._pool = await asyncio.wait_for(
87
+ self._create_pool(),
88
+ timeout=self._config.connection_timeout_seconds,
89
+ )
90
+ _logger.info(
91
+ "Created pool for %s (attempt %d)",
92
+ self._config.safe_repr, attempt + 1,
93
+ )
94
+ return self._pool
95
+ except asyncio.TimeoutError:
96
+ last_exc = TimeoutError(
97
+ f"Connection timed out after {self._config.connection_timeout_seconds}s"
98
+ )
99
+ _logger.warning(
100
+ "Pool creation timeout for %s (attempt %d)",
101
+ self._config.safe_repr, attempt + 1,
102
+ )
103
+ except Exception as exc:
104
+ last_exc = exc
105
+ _logger.warning(
106
+ "Pool creation failed for %s (attempt %d): %s",
107
+ self._config.safe_repr, attempt + 1, exc,
108
+ )
109
+
110
+ if attempt < self._max_connection_retries - 1:
111
+ wait = 0.1 * (2**attempt)
112
+ await asyncio.sleep(wait)
113
+
114
+ msg = (
115
+ f"Failed to create connection pool after {self._max_connection_retries} attempts"
116
+ )
117
+ if last_exc is not None:
118
+ msg += f": {last_exc}"
119
+ raise RuntimeError(msg) from last_exc
120
+
121
+ async def close(self) -> None:
122
+ async with self._lock:
123
+ if self._closed:
124
+ return
125
+ self._closed = True
126
+ if self._pool is not None:
127
+ await self._close_pool(self._pool)
128
+ self._pool = None
129
+ _logger.info("Closed pool for %s", self._config.safe_repr)
130
+
131
+ @abstractmethod
132
+ async def _close_pool(self, pool: Any) -> None:
133
+ ...
134
+
135
+ @property
136
+ def is_closed(self) -> bool:
137
+ return self._closed
app/core/database/mongodb.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from urllib.parse import quote_plus
5
+
6
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
7
+
8
+ from app.core.database.base import BaseExecutor, ConnectionConfig, StatementResult
9
+ from app.core.logger import get_logger
10
+
11
+ _logger = get_logger(__name__)
12
+
13
+ _ATLAS_SRV_DOMAINS = frozenset({"mongodb.net", "mongodbatlas.com"})
14
+ _DATABASE_LEVEL_MARKER = "$database"
15
+
16
+
17
+ class MongoDBExecutor(BaseExecutor):
18
+ async def _create_pool(self) -> AsyncIOMotorClient:
19
+ client_kwargs: dict[str, Any] = {
20
+ "serverSelectionTimeoutMS": int(
21
+ self._config.connection_timeout_seconds * 1000
22
+ ),
23
+ "connectTimeoutMS": int(self._config.connection_timeout_seconds * 1000),
24
+ "maxPoolSize": 10,
25
+ "minPoolSize": 1,
26
+ }
27
+
28
+ if self._config.ssl_enabled:
29
+ client_kwargs["tls"] = True
30
+
31
+ host = self._config.host
32
+
33
+ if self._is_atlas_srv(host):
34
+ return self._connect_via_uri(client_kwargs)
35
+
36
+ client_kwargs["host"] = host
37
+ client_kwargs["port"] = self._config.port
38
+ client_kwargs["username"] = self._config.username
39
+ client_kwargs["password"] = self._config.password
40
+
41
+ return AsyncIOMotorClient(**client_kwargs)
42
+
43
+ def _connect_via_uri(self, client_kwargs: dict[str, Any]) -> AsyncIOMotorClient:
44
+ escaped_user = quote_plus(self._config.username)
45
+ escaped_pass = quote_plus(self._config.password)
46
+ uri = (
47
+ f"mongodb+srv://{escaped_user}:{escaped_pass}@{self._config.host}/"
48
+ f"{self._config.database}?retryWrites=true&w=majority"
49
+ )
50
+ client_kwargs.pop("tls", None)
51
+ return AsyncIOMotorClient(uri, **client_kwargs)
52
+
53
+ @staticmethod
54
+ def _is_atlas_srv(host: str) -> bool:
55
+ host_lower = host.lower()
56
+ for domain in _ATLAS_SRV_DOMAINS:
57
+ if domain in host_lower:
58
+ return True
59
+ return host_lower.startswith("mongodb+srv://")
60
+
61
+ async def _execute_queries(
62
+ self,
63
+ pool: AsyncIOMotorClient,
64
+ queries: list[dict[str, Any]],
65
+ use_transaction: bool,
66
+ ) -> list[StatementResult]:
67
+ db: AsyncIOMotorDatabase = pool[self._config.database]
68
+ results: list[StatementResult] = []
69
+ for query_item in queries:
70
+ collection_name = query_item.get("collection", _DATABASE_LEVEL_MARKER)
71
+ pipeline = query_item.get("pipeline", [])
72
+ try:
73
+ if collection_name == _DATABASE_LEVEL_MARKER:
74
+ cursor = db.aggregate(pipeline)
75
+ else:
76
+ cursor = db[collection_name].aggregate(pipeline)
77
+
78
+ data = []
79
+ async for doc in cursor:
80
+ if "_id" in doc:
81
+ doc["_id"] = str(doc["_id"])
82
+ data.append(doc)
83
+ if len(data) >= self._config.max_rows:
84
+ break
85
+ results.append(
86
+ StatementResult(success=True, rows=len(data), data=data)
87
+ )
88
+ except Exception as exc:
89
+ results.append(
90
+ StatementResult(
91
+ success=False,
92
+ error=str(exc),
93
+ error_code=type(exc).__name__,
94
+ )
95
+ )
96
+ break
97
+ return results
98
+
99
+ async def _close_pool(self, pool: AsyncIOMotorClient) -> None:
100
+ pool.close()
101
+
102
+ async def _get_or_create_pool(self) -> AsyncIOMotorClient:
103
+ if self._pool is not None:
104
+ return self._pool
105
+ return await super()._get_or_create_pool()
app/core/database/mysql.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import sys
5
+ from typing import Any
6
+
7
+ import aiomysql
8
+
9
+ from app.core.database.base import BaseExecutor, ConnectionConfig, StatementResult
10
+ from app.core.logger import get_logger
11
+
12
+ _logger = get_logger(__name__)
13
+
14
+
15
+ class MySQLExecutor(BaseExecutor):
16
+ async def _create_pool(self) -> aiomysql.Pool:
17
+ return await aiomysql.create_pool(
18
+ host=self._config.host,
19
+ port=self._config.port,
20
+ user=self._config.username,
21
+ password=self._config.password,
22
+ db=self._config.database,
23
+ minsize=1,
24
+ maxsize=10,
25
+ autocommit=False,
26
+ connect_timeout=self._config.connection_timeout_seconds,
27
+ pool_recycle=3600,
28
+ )
29
+
30
+ async def _get_or_create_pool(self) -> aiomysql.Pool:
31
+ async with self._lock:
32
+ if self._pool is not None:
33
+ return self._pool
34
+
35
+ last_error: Exception | None = None
36
+ for attempt in range(self._max_connection_retries):
37
+ try:
38
+ self._pool = await asyncio.wait_for(
39
+ self._create_pool(),
40
+ timeout=self._config.connection_timeout_seconds,
41
+ )
42
+ return self._pool
43
+ except Exception as exc:
44
+ last_error = exc
45
+ _logger.warning(
46
+ "MySQL connection failed (attempt %d): %s",
47
+ attempt + 1, exc,
48
+ )
49
+ if attempt < self._max_connection_retries - 1:
50
+ await asyncio.sleep(0.1 * (2**attempt))
51
+
52
+ raise RuntimeError(
53
+ f"Failed to connect to MySQL after {self._max_connection_retries} attempts"
54
+ ) from last_error
55
+
56
+ async def _execute_queries(
57
+ self, pool: aiomysql.Pool, queries: list[str], use_transaction: bool
58
+ ) -> list[StatementResult]:
59
+ async with pool.acquire() as conn:
60
+ if use_transaction:
61
+ await conn.begin()
62
+ try:
63
+ results: list[StatementResult] = []
64
+ for query in queries:
65
+ result = await self._execute_one(conn, query)
66
+ results.append(result)
67
+ if not result.success and use_transaction:
68
+ await conn.rollback()
69
+ return results
70
+ if use_transaction:
71
+ await conn.commit()
72
+ return results
73
+ except Exception:
74
+ if use_transaction:
75
+ try:
76
+ await conn.rollback()
77
+ except Exception:
78
+ pass
79
+ raise
80
+
81
+ async def _execute_one(self, conn: Any, query: str) -> StatementResult:
82
+ try:
83
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
84
+ await cursor.execute(query)
85
+ if cursor.description:
86
+ rows = await cursor.fetchall()
87
+ data = [dict(row) for row in rows][: self._config.max_rows]
88
+ return StatementResult(success=True, rows=len(data), data=data)
89
+ await conn.commit()
90
+ return StatementResult(success=True, rows=cursor.rowcount, data=[])
91
+ except Exception as exc:
92
+ error_code = getattr(exc, "args", [None])[0]
93
+ if isinstance(error_code, int):
94
+ error_code = str(error_code)
95
+ else:
96
+ error_code = type(exc).__name__
97
+ return StatementResult(success=False, error=str(exc), error_code=error_code)
98
+
99
+ async def _close_pool(self, pool: aiomysql.Pool) -> None:
100
+ pool.close()
101
+ await pool.wait_closed()
app/core/database/pool.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from app.core.database.base import BaseExecutor, ConnectionConfig
7
+ from app.core.database.mysql import MySQLExecutor
8
+ from app.core.database.postgresql import PostgreSQLExecutor
9
+ from app.core.database.mongodb import MongoDBExecutor
10
+ from app.core.logger import get_logger
11
+
12
+ _logger = get_logger(__name__)
13
+
14
+ _EXECUTOR_MAP: dict[str, type[BaseExecutor]] = {
15
+ "mysql": MySQLExecutor,
16
+ "postgresql": PostgreSQLExecutor,
17
+ "mongodb": MongoDBExecutor,
18
+ }
19
+
20
+
21
+ class PoolManager:
22
+ def __init__(self) -> None:
23
+ self._executors: dict[str, BaseExecutor] = {}
24
+ self._lock = asyncio.Lock()
25
+ self._closed = False
26
+
27
+ async def get_executor(self, config: ConnectionConfig) -> BaseExecutor:
28
+ if self._closed:
29
+ raise RuntimeError("PoolManager has been shut down")
30
+
31
+ key = config.pool_key
32
+ async with self._lock:
33
+ if key not in self._executors:
34
+ executor_cls = _EXECUTOR_MAP.get(config.db_type)
35
+ if executor_cls is None:
36
+ raise ValueError(f"Unsupported database type: {config.db_type}")
37
+ self._executors[key] = executor_cls(config)
38
+ _logger.info(
39
+ "Created executor for %s (key=%s)", config.safe_repr, key,
40
+ )
41
+ return self._executors[key]
42
+
43
+ async def close_all(self) -> None:
44
+ async with self._lock:
45
+ if self._closed:
46
+ return
47
+ self._closed = True
48
+ for key, executor in self._executors.items():
49
+ try:
50
+ await executor.close()
51
+ except Exception as exc:
52
+ _logger.error("Error closing executor %s: %s", key, exc)
53
+ self._executors.clear()
54
+ _logger.info("All database executors closed")
55
+
56
+
57
+ pool_manager = PoolManager()
app/core/database/postgresql.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import ssl as ssl_module
4
+ from typing import Any
5
+
6
+ import asyncpg
7
+
8
+ from app.core.database.base import BaseExecutor, ConnectionConfig, StatementResult
9
+ from app.core.logger import get_logger
10
+
11
+ _logger = get_logger(__name__)
12
+
13
+
14
+ class PostgreSQLExecutor(BaseExecutor):
15
+ async def _create_pool(self) -> asyncpg.Pool:
16
+ ssl_ctx: ssl_module.SSLContext | None = None
17
+ if self._config.ssl_enabled:
18
+ ssl_ctx = self._build_ssl_context()
19
+
20
+ return await asyncpg.create_pool(
21
+ host=self._config.host,
22
+ port=self._config.port,
23
+ user=self._config.username,
24
+ password=self._config.password,
25
+ database=self._config.database,
26
+ min_size=1,
27
+ max_size=10,
28
+ timeout=self._config.connection_timeout_seconds,
29
+ ssl=ssl_ctx,
30
+ max_inactive_connection_lifetime=3600.0,
31
+ )
32
+
33
+ async def _execute_queries(
34
+ self, pool: asyncpg.Pool, queries: list[str], use_transaction: bool
35
+ ) -> list[StatementResult]:
36
+ async with pool.acquire() as conn:
37
+ if use_transaction:
38
+ tr = conn.transaction()
39
+ await tr.start()
40
+ try:
41
+ results: list[StatementResult] = []
42
+ for query in queries:
43
+ result = await self._execute_one(conn, query)
44
+ results.append(result)
45
+ if not result.success and use_transaction:
46
+ await tr.rollback()
47
+ _logger.warning(
48
+ "Query failed, transaction rolled back: %s",
49
+ result.error,
50
+ )
51
+ return results
52
+ if use_transaction:
53
+ await tr.commit()
54
+ return results
55
+ except Exception as exc:
56
+ if use_transaction:
57
+ try:
58
+ await tr.rollback()
59
+ except Exception:
60
+ pass
61
+ raise
62
+
63
+ async def _execute_one(self, conn: asyncpg.Connection, query: str) -> StatementResult:
64
+ try:
65
+ stripped = query.strip().upper()
66
+ if stripped.startswith("SELECT") or stripped.startswith("WITH"):
67
+ rows = await conn.fetch(query)
68
+ data = [dict(row) for row in rows][: self._config.max_rows]
69
+ return StatementResult(success=True, rows=len(data), data=data)
70
+
71
+ if "RETURNING" in stripped:
72
+ rows = await conn.fetch(query)
73
+ data = [dict(row) for row in rows][: self._config.max_rows]
74
+ return StatementResult(success=True, rows=len(data), data=data)
75
+
76
+ result = await conn.execute(query)
77
+ parts = result.split()
78
+ rowcount = int(parts[-1]) if parts[-1].isdigit() else 0
79
+ return StatementResult(success=True, rows=rowcount, data=[])
80
+ except Exception as exc:
81
+ return StatementResult(
82
+ success=False,
83
+ error=str(exc),
84
+ error_code=type(exc).__name__,
85
+ )
86
+
87
+ async def _close_pool(self, pool: asyncpg.Pool) -> None:
88
+ await pool.close()
89
+
90
+ @staticmethod
91
+ def _build_ssl_context() -> ssl_module.SSLContext:
92
+ ctx = ssl_module.create_default_context()
93
+ return ctx
app/models/schemas.py CHANGED
@@ -1,8 +1,8 @@
1
  from __future__ import annotations
2
 
3
- from typing import Any, Dict, List, Optional
4
 
5
- from pydantic import BaseModel, Field, field_validator
6
 
7
 
8
  class ConversionMetadata(BaseModel):
@@ -107,3 +107,118 @@ class SpacyLabelsResponse(BaseModel):
107
  spacy_labels: Dict[str, str]
108
  source_types: Dict[str, str]
109
  example_mappings: Dict[str, Dict[str, Any]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ from typing import Any, Dict, List, Literal, Optional
4
 
5
+ from pydantic import BaseModel, Field, field_validator, model_validator
6
 
7
 
8
  class ConversionMetadata(BaseModel):
 
107
  spacy_labels: Dict[str, str]
108
  source_types: Dict[str, str]
109
  example_mappings: Dict[str, Dict[str, Any]]
110
+
111
+
112
+ class SSLConfig(BaseModel):
113
+ enabled: bool = False
114
+ ca_cert: Optional[str] = None
115
+ cert: Optional[str] = None
116
+ key: Optional[str] = None
117
+
118
+
119
+ class DatabaseConnection(BaseModel):
120
+ host: str
121
+ port: Optional[int] = None
122
+ database: str
123
+ username: str
124
+ password: str = Field(repr=False)
125
+ ssl: SSLConfig = SSLConfig()
126
+
127
+ @field_validator("host")
128
+ @classmethod
129
+ def host_not_empty(cls, v: str) -> str:
130
+ stripped = v.strip()
131
+ if not stripped:
132
+ raise ValueError("host must not be empty")
133
+ return stripped
134
+
135
+ @field_validator("port")
136
+ @classmethod
137
+ def validate_port(cls, v: Optional[int]) -> Optional[int]:
138
+ if v is not None and (v < 1 or v > 65535):
139
+ raise ValueError("port must be between 1 and 65535")
140
+ return v
141
+
142
+ @field_validator("database")
143
+ @classmethod
144
+ def database_not_empty(cls, v: str) -> str:
145
+ stripped = v.strip()
146
+ if not stripped:
147
+ raise ValueError("database must not be empty")
148
+ return stripped
149
+
150
+ def safe_repr(self) -> str:
151
+ return (
152
+ f"host={self.host}, port={self.port}, "
153
+ f"database={self.database}, username={self.username}"
154
+ )
155
+
156
+
157
+ class DatabaseQueryRequest(BaseModel):
158
+ db_type: Literal["mysql", "postgresql", "mongodb"]
159
+ connection: DatabaseConnection
160
+ query: List[Any]
161
+ use_transaction: bool = True
162
+ query_timeout_seconds: float = Field(default=10.0, ge=1.0, le=300.0)
163
+ connection_timeout_seconds: float = Field(default=10.0, ge=1.0, le=60.0)
164
+ max_rows: int = Field(default=10000, ge=1, le=1_000_000)
165
+
166
+ @model_validator(mode="after")
167
+ def validate_query(self) -> "DatabaseQueryRequest":
168
+ if not self.query:
169
+ raise ValueError("query must not be empty")
170
+ if self.db_type in ("mysql", "postgresql"):
171
+ for i, q in enumerate(self.query):
172
+ if not isinstance(q, str):
173
+ raise ValueError(f"query[{i}] must be a string for {self.db_type}")
174
+ if not q.strip():
175
+ raise ValueError(f"query[{i}] must not be empty")
176
+ elif self.db_type == "mongodb":
177
+ for i, q in enumerate(self.query):
178
+ if not isinstance(q, dict):
179
+ raise ValueError(f"query[{i}] must be an object with 'collection' and 'pipeline'")
180
+ if "collection" not in q or "pipeline" not in q:
181
+ raise ValueError(f"query[{i}] must have 'collection' and 'pipeline' fields")
182
+ if not isinstance(q["pipeline"], list):
183
+ raise ValueError(f"query[{i}].pipeline must be an array")
184
+ return self
185
+
186
+ def to_connection_config(self) -> Dict[str, Any]:
187
+ return {
188
+ "db_type": self.db_type,
189
+ "host": self.connection.host,
190
+ "port": self.connection.port or self._default_port(),
191
+ "database": self.connection.database,
192
+ "username": self.connection.username,
193
+ "password": self.connection.password,
194
+ "ssl_enabled": self.connection.ssl.enabled,
195
+ "ssl_ca_cert": self.connection.ssl.ca_cert,
196
+ "ssl_cert": self.connection.ssl.cert,
197
+ "ssl_key": self.connection.ssl.key,
198
+ "query_timeout_seconds": self.query_timeout_seconds,
199
+ "connection_timeout_seconds": self.connection_timeout_seconds,
200
+ "max_rows": self.max_rows,
201
+ }
202
+
203
+ def _default_port(self) -> int:
204
+ return {"mysql": 3306, "postgresql": 5432, "mongodb": 27017}[self.db_type]
205
+
206
+
207
+ class StatementResultSchema(BaseModel):
208
+ success: bool
209
+ rows: int = 0
210
+ data: List[Dict[str, Any]] = []
211
+ error: Optional[str] = None
212
+ error_code: Optional[str] = None
213
+
214
+
215
+ class DatabaseQueryError(BaseModel):
216
+ message: str
217
+ code: Optional[str] = None
218
+
219
+
220
+ class DatabaseQueryResponse(BaseModel):
221
+ success: bool
222
+ execution_time_ms: float
223
+ results: Optional[List[StatementResultSchema]] = None
224
+ error: Optional[DatabaseQueryError] = None
app/services/database_service.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from app.core.database import ConnectionConfig, pool_manager
7
+ from app.core.database.base import StatementResult
8
+ from app.core.logger import get_logger
9
+ from app.models.schemas import (
10
+ DatabaseQueryError,
11
+ DatabaseQueryRequest,
12
+ DatabaseQueryResponse,
13
+ StatementResultSchema,
14
+ )
15
+
16
+ _logger = get_logger(__name__)
17
+
18
+
19
+ def _root_cause(exc: Exception) -> str:
20
+ cause = exc.__cause__ or exc.__context__
21
+ if cause:
22
+ return f"{exc} [{cause}]"
23
+ return str(exc)
24
+
25
+
26
+ class DatabaseService:
27
+
28
+ async def execute_query(self, request: DatabaseQueryRequest) -> DatabaseQueryResponse:
29
+ start_time = time.monotonic()
30
+ config = ConnectionConfig(**request.to_connection_config())
31
+
32
+ try:
33
+ executor = await pool_manager.get_executor(config)
34
+ except Exception as exc:
35
+ elapsed = (time.monotonic() - start_time) * 1000
36
+ _logger.error(
37
+ "Failed to acquire executor for %s: %s",
38
+ config.safe_repr, exc,
39
+ )
40
+ return DatabaseQueryResponse(
41
+ success=False,
42
+ execution_time_ms=round(elapsed, 2),
43
+ error=DatabaseQueryError(
44
+ message=f"Connection failed: {_root_cause(exc)}",
45
+ code=type(exc).__name__,
46
+ ),
47
+ )
48
+
49
+ try:
50
+ results = await executor.execute(
51
+ request.query,
52
+ use_transaction=request.use_transaction,
53
+ )
54
+ except Exception as exc:
55
+ elapsed = (time.monotonic() - start_time) * 1000
56
+ _logger.error(
57
+ "Query execution failed for %s: %s",
58
+ config.safe_repr, exc,
59
+ )
60
+ return DatabaseQueryResponse(
61
+ success=False,
62
+ execution_time_ms=round(elapsed, 2),
63
+ error=DatabaseQueryError(
64
+ message=f"Execution failed: {_root_cause(exc)}",
65
+ code=type(exc).__name__,
66
+ ),
67
+ )
68
+
69
+ elapsed = (time.monotonic() - start_time) * 1000
70
+
71
+ statement_results = [
72
+ StatementResultSchema(
73
+ success=r.success,
74
+ rows=r.rows,
75
+ data=r.data,
76
+ error=r.error,
77
+ error_code=r.error_code,
78
+ )
79
+ for r in results
80
+ ]
81
+
82
+ overall_success = all(r.success for r in results)
83
+
84
+ if overall_success:
85
+ _logger.info(
86
+ "Query success for %s (%d stmts, %.2fms)",
87
+ config.safe_repr, len(results), elapsed,
88
+ )
89
+ return DatabaseQueryResponse(
90
+ success=True,
91
+ execution_time_ms=round(elapsed, 2),
92
+ results=statement_results,
93
+ )
94
+
95
+ _logger.warning(
96
+ "Query partial/full failure for %s (%d stmts, %.2fms)",
97
+ config.safe_repr, len(results), elapsed,
98
+ )
99
+ return DatabaseQueryResponse(
100
+ success=False,
101
+ execution_time_ms=round(elapsed, 2),
102
+ results=statement_results,
103
+ )
main.py CHANGED
@@ -7,7 +7,7 @@ from app.core.logger import get_logger
7
  logger = get_logger(__name__)
8
 
9
  if __name__ == "__main__":
10
- logger.info("Starting %s server", "reconciliation-non-ai-extractor")
11
  uvicorn.run(
12
  "app.api.server:app",
13
  host="0.0.0.0",
 
7
  logger = get_logger(__name__)
8
 
9
  if __name__ == "__main__":
10
+ logger.info("Starting %s server", "All API Collection")
11
  uvicorn.run(
12
  "app.api.server:app",
13
  host="0.0.0.0",
postman_collection.json DELETED
@@ -1,387 +0,0 @@
1
- {
2
- "info": {
3
- "_postman_id": "reconciliation-not-ai-extractor-service",
4
- "name": "Reconciliation-not-ai-extractor-service",
5
- "description": "Production-ready Postman collection for the Document-to-Markdown and structured data extraction API powered by Microsoft MarkItDown, RapidOCR, and spaCy.\n\n**Base URL:** `{{base_url}}`\n**Authentication:** API key via `x-api-key` header\n\n## Endpoints\n- **System:** Health, info, supported formats, spaCy labels\n- **Convert:** Single file and single URL to Markdown\n- **Batch:** Bulk conversion of files and URLs",
6
- "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7
- },
8
- "item": [
9
- {
10
- "name": "System",
11
- "description": "Health checks, server metadata, supported formats, and spaCy NER labels.",
12
- "item": [
13
- {
14
- "name": "Root Health (No Auth)",
15
- "request": {
16
- "method": "GET",
17
- "header": [],
18
- "url": {
19
- "raw": "{{base_url}}/health",
20
- "host": [
21
- "{{base_url}}"
22
- ],
23
- "path": [
24
- "health"
25
- ]
26
- },
27
- "description": "Quick health check without authentication."
28
- },
29
- "response": []
30
- },
31
- {
32
- "name": "Ping (No Auth)",
33
- "request": {
34
- "method": "GET",
35
- "header": [],
36
- "url": {
37
- "raw": "{{base_url}}/ping",
38
- "host": [
39
- "{{base_url}}"
40
- ],
41
- "path": [
42
- "ping"
43
- ]
44
- },
45
- "description": "Simple ping endpoint without authentication."
46
- },
47
- "response": []
48
- },
49
- {
50
- "name": "Health",
51
- "request": {
52
- "method": "GET",
53
- "header": [
54
- {
55
- "key": "x-api-key",
56
- "value": "{{api_key}}",
57
- "type": "text"
58
- }
59
- ],
60
- "url": {
61
- "raw": "{{base_url}}/v1/health",
62
- "host": [
63
- "{{base_url}}"
64
- ],
65
- "path": [
66
- "v1",
67
- "health"
68
- ]
69
- },
70
- "description": "Authenticated health check."
71
- },
72
- "response": []
73
- },
74
- {
75
- "name": "Info",
76
- "request": {
77
- "method": "GET",
78
- "header": [
79
- {
80
- "key": "x-api-key",
81
- "value": "{{api_key}}",
82
- "type": "text"
83
- }
84
- ],
85
- "url": {
86
- "raw": "{{base_url}}/v1/info",
87
- "host": [
88
- "{{base_url}}"
89
- ],
90
- "path": [
91
- "v1",
92
- "info"
93
- ]
94
- },
95
- "description": "Server and environment information."
96
- },
97
- "response": []
98
- },
99
- {
100
- "name": "Supported Formats",
101
- "request": {
102
- "method": "GET",
103
- "header": [
104
- {
105
- "key": "x-api-key",
106
- "value": "{{api_key}}",
107
- "type": "text"
108
- }
109
- ],
110
- "url": {
111
- "raw": "{{base_url}}/v1/formats",
112
- "host": [
113
- "{{base_url}}"
114
- ],
115
- "path": [
116
- "v1",
117
- "formats"
118
- ]
119
- },
120
- "description": "List all supported file formats."
121
- },
122
- "response": []
123
- },
124
- {
125
- "name": "spaCy Labels",
126
- "request": {
127
- "method": "GET",
128
- "header": [
129
- {
130
- "key": "x-api-key",
131
- "value": "{{api_key}}",
132
- "type": "text"
133
- }
134
- ],
135
- "url": {
136
- "raw": "{{base_url}}/v1/spacy-labels",
137
- "host": [
138
- "{{base_url}}"
139
- ],
140
- "path": [
141
- "v1",
142
- "spacy-labels"
143
- ]
144
- },
145
- "description": "Available spaCy NER labels and mappings."
146
- },
147
- "response": []
148
- }
149
- ]
150
- },
151
- {
152
- "name": "Convert",
153
- "description": "Single-file and single-URL conversion to Markdown with optional structured JSON extraction.",
154
- "item": [
155
- {
156
- "name": "Convert File",
157
- "request": {
158
- "method": "POST",
159
- "header": [
160
- {
161
- "key": "x-api-key",
162
- "value": "{{api_key}}",
163
- "type": "text"
164
- }
165
- ],
166
- "body": {
167
- "mode": "formdata",
168
- "formdata": [
169
- {
170
- "key": "file",
171
- "type": "file",
172
- "src": "/path/to/your/document.pdf",
173
- "description": "The file to convert"
174
- },
175
- {
176
- "key": "plain_text",
177
- "value": "false",
178
- "type": "text"
179
- },
180
- {
181
- "key": "return_json",
182
- "value": "false",
183
- "type": "text"
184
- },
185
- {
186
- "key": "mappings",
187
- "value": "{\"company\":{\"source_type\":\"entity\",\"label\":\"ORG\"}}",
188
- "type": "text"
189
- }
190
- ]
191
- },
192
- "url": {
193
- "raw": "{{base_url}}/v1/convert/file",
194
- "host": [
195
- "{{base_url}}"
196
- ],
197
- "path": [
198
- "v1",
199
- "convert",
200
- "file"
201
- ]
202
- },
203
- "description": "Upload a single file and convert it to Markdown."
204
- },
205
- "response": []
206
- },
207
- {
208
- "name": "Convert URL",
209
- "request": {
210
- "method": "POST",
211
- "header": [
212
- {
213
- "key": "x-api-key",
214
- "value": "{{api_key}}",
215
- "type": "text"
216
- },
217
- {
218
- "key": "Content-Type",
219
- "value": "application/json",
220
- "type": "text"
221
- }
222
- ],
223
- "body": {
224
- "mode": "raw",
225
- "raw": "{\n \"url\": \"https://example.com/document.pdf\",\n \"return_json\": false,\n \"mappings\": {\n \"company\": {\n \"source_type\": \"entity\",\n \"label\": \"ORG\"\n }\n }\n}",
226
- "options": {
227
- "raw": {
228
- "language": "json"
229
- }
230
- }
231
- },
232
- "url": {
233
- "raw": "{{base_url}}/v1/convert/url",
234
- "host": [
235
- "{{base_url}}"
236
- ],
237
- "path": [
238
- "v1",
239
- "convert",
240
- "url"
241
- ]
242
- },
243
- "description": "Convert a remote document URL to Markdown."
244
- },
245
- "response": []
246
- }
247
- ]
248
- },
249
- {
250
- "name": "Batch",
251
- "description": "Bulk conversion of multiple files and URLs.",
252
- "item": [
253
- {
254
- "name": "Batch Files",
255
- "request": {
256
- "method": "POST",
257
- "header": [
258
- {
259
- "key": "x-api-key",
260
- "value": "{{api_key}}",
261
- "type": "text"
262
- }
263
- ],
264
- "body": {
265
- "mode": "formdata",
266
- "formdata": [
267
- {
268
- "key": "files",
269
- "type": "file",
270
- "src": "/path/to/document1.pdf"
271
- },
272
- {
273
- "key": "files",
274
- "type": "file",
275
- "src": "/path/to/document2.docx"
276
- }
277
- ]
278
- },
279
- "url": {
280
- "raw": "{{base_url}}/v1/batch/files",
281
- "host": [
282
- "{{base_url}}"
283
- ],
284
- "path": [
285
- "v1",
286
- "batch",
287
- "files"
288
- ]
289
- },
290
- "description": "Upload and convert multiple files."
291
- },
292
- "response": []
293
- },
294
- {
295
- "name": "Batch URLs",
296
- "request": {
297
- "method": "POST",
298
- "header": [
299
- {
300
- "key": "x-api-key",
301
- "value": "{{api_key}}",
302
- "type": "text"
303
- },
304
- {
305
- "key": "Content-Type",
306
- "value": "application/json",
307
- "type": "text"
308
- }
309
- ],
310
- "body": {
311
- "mode": "raw",
312
- "raw": "{\n \"urls\": [\n \"https://example.com/report1.pdf\",\n \"https://example.com/report2.docx\"\n ]\n}",
313
- "options": {
314
- "raw": {
315
- "language": "json"
316
- }
317
- }
318
- },
319
- "url": {
320
- "raw": "{{base_url}}/v1/batch/urls",
321
- "host": [
322
- "{{base_url}}"
323
- ],
324
- "path": [
325
- "v1",
326
- "batch",
327
- "urls"
328
- ]
329
- },
330
- "description": "Convert multiple URLs."
331
- },
332
- "response": []
333
- }
334
- ]
335
- }
336
- ],
337
- "event": [
338
- {
339
- "listen": "prerequest",
340
- "script": {
341
- "type": "text/javascript",
342
- "exec": [
343
- "// Validate required collection variables before each request",
344
- "const baseUrl = pm.collectionVariables.get('base_url');",
345
- "const apiKey = pm.collectionVariables.get('api_key');",
346
- "",
347
- "if (!baseUrl) {",
348
- " pm.expect.fail('Missing collection variable: base_url');",
349
- "}",
350
- "",
351
- "if (!apiKey && pm.request.url.path.join('/').startsWith('v1')) {",
352
- " pm.expect.fail('Missing collection variable: api_key');",
353
- "}"
354
- ]
355
- }
356
- }
357
- ],
358
- "variable": [
359
- {
360
- "key": "base_url",
361
- "value": "http://localhost:7860",
362
- "type": "string",
363
- "description": "Base URL of the service"
364
- },
365
- {
366
- "key": "api_key",
367
- "value": "changeme",
368
- "type": "string",
369
- "description": "API key for authenticated endpoints"
370
- }
371
- ],
372
- "auth": {
373
- "type": "apikey",
374
- "apikey": [
375
- {
376
- "key": "value",
377
- "value": "{{api_key}}",
378
- "type": "string"
379
- },
380
- {
381
- "key": "key",
382
- "value": "x-api-key",
383
- "type": "string"
384
- }
385
- ]
386
- }
387
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -3,9 +3,9 @@ requires = ["setuptools>=70", "wheel"]
3
  build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
- name = "reconciliation-non-ai-extractor"
7
  version = "1.0.0"
8
- description = "Enterprise document extraction API powered by Microsoft MarkItDown, RapidOCR, and spaCy"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
11
  license = { text = "MIT" }
@@ -34,4 +34,4 @@ dev = ["pytest>=8", "pytest-asyncio>=0.23"]
34
 
35
  [tool.setuptools.packages.find]
36
  where = ["."]
37
- include = ["reconciliation_non_ai_extractor*"]
 
3
  build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
+ name = "All API Collection"
7
  version = "1.0.0"
8
+ description = "All API Collection - Document extraction, conversion, and database query API"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
11
  license = { text = "MIT" }
 
34
 
35
  [tool.setuptools.packages.find]
36
  where = ["."]
37
+ include = ["All API Collection*"]
requirements.txt CHANGED
@@ -12,3 +12,8 @@ pillow>=10.0.0
12
  pypdfium2>=4.30.0
13
  pandas>=2.0.0
14
  spacy>=3.7.0
 
 
 
 
 
 
12
  pypdfium2>=4.30.0
13
  pandas>=2.0.0
14
  spacy>=3.7.0
15
+
16
+ # Async database drivers
17
+ aiomysql>=0.3.2
18
+ asyncpg>=0.31.0
19
+ motor>=3.7.1