mineru-parser 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. mineru_parser/__init__.py +41 -0
  2. mineru_parser/_client/__init__.py +47 -0
  3. mineru_parser/_client/_telemetry.py +7 -0
  4. mineru_parser/_client/_time.py +13 -0
  5. mineru_parser/_client/_validation.py +161 -0
  6. mineru_parser/_client/api.py +233 -0
  7. mineru_parser/_client/async_result.py +254 -0
  8. mineru_parser/_client/errors.py +138 -0
  9. mineru_parser/_client/models.py +211 -0
  10. mineru_parser/_client/polling.py +309 -0
  11. mineru_parser/_client/transport.py +289 -0
  12. mineru_parser/_downloader/__init__.py +40 -0
  13. mineru_parser/_downloader/downloader.py +1099 -0
  14. mineru_parser/_downloader/errors.py +122 -0
  15. mineru_parser/_downloader/policy.py +89 -0
  16. mineru_parser/_downloader/security.py +380 -0
  17. mineru_parser/_extractor/__init__.py +38 -0
  18. mineru_parser/_extractor/_manifest.py +127 -0
  19. mineru_parser/_extractor/_members.py +351 -0
  20. mineru_parser/_extractor/errors.py +130 -0
  21. mineru_parser/_extractor/extractor.py +310 -0
  22. mineru_parser/_extractor/limits.py +35 -0
  23. mineru_parser/_extractor/models.py +31 -0
  24. mineru_parser/_http.py +77 -0
  25. mineru_parser/_telemetry.py +62 -0
  26. mineru_parser/_version.py +10 -0
  27. mineru_parser/errors.py +15 -0
  28. mineru_parser/logging.py +0 -0
  29. mineru_parser/pipeline/__init__.py +29 -0
  30. mineru_parser/pipeline/budget.py +109 -0
  31. mineru_parser/pipeline/errors.py +64 -0
  32. mineru_parser/pipeline/orchestrator.py +586 -0
  33. mineru_parser/pipeline/result.py +32 -0
  34. mineru_parser/pipeline/stages.py +22 -0
  35. mineru_parser/schema/content_v2.py +198 -0
  36. mineru_parser-0.1.0.dist-info/METADATA +409 -0
  37. mineru_parser-0.1.0.dist-info/RECORD +39 -0
  38. mineru_parser-0.1.0.dist-info/WHEEL +4 -0
  39. mineru_parser-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,41 @@
1
+ """MinerU Parser 公共 API。
2
+
3
+ 整个库对外只暴露一个函数 :func:`parse`(SPEC-004 AC-2.1 / AC-2.6);
4
+ 其余导出都是 ``parse`` 的签名或返回值所引用的类型、异常类与版本常量。
5
+ 三个组件(client / downloader / extractor)是私有实现,不提供公开导入路径
6
+ (SPEC-004 AC-2.3)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from ._client.models import MinerUTaskStatus
12
+ from ._extractor.limits import ExtractionLimits
13
+ from ._extractor.models import ExtractedImage, ExtractedResult
14
+ from ._telemetry import MetricHook
15
+ from ._version import __version__
16
+ from .errors import MinerUParserError
17
+ from .pipeline import parse
18
+ from .pipeline.errors import (
19
+ PipelineConfigurationError,
20
+ PipelineError,
21
+ PipelineTimeoutError,
22
+ )
23
+ from .pipeline.result import ErrorMessage, ParseResult
24
+ from .pipeline.stages import PipelineStage
25
+
26
+ __all__ = [
27
+ "ErrorMessage",
28
+ "ExtractedImage",
29
+ "ExtractedResult",
30
+ "ExtractionLimits",
31
+ "MetricHook",
32
+ "MinerUParserError",
33
+ "MinerUTaskStatus",
34
+ "ParseResult",
35
+ "PipelineConfigurationError",
36
+ "PipelineError",
37
+ "PipelineStage",
38
+ "PipelineTimeoutError",
39
+ "__version__",
40
+ "parse",
41
+ ]
@@ -0,0 +1,47 @@
1
+ """MinerU 客户端实现子包(内部实现,不对外)。
2
+
3
+ 本包是 SPEC-001 的私有实现,仅由 :mod:`mineru_parser.pipeline` 内部使用;
4
+ 不提供公开导入路径,也不遵循 SemVer。行为契约见 SPEC-001。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ._telemetry import MetricHook
10
+ from .api import MinerUApiClient, MinerUClient
11
+ from .async_result import AsyncResult
12
+ from .errors import (
13
+ MinerUAuthenticationError,
14
+ MinerUConfigurationError,
15
+ MinerUError,
16
+ MinerUExecutionError,
17
+ MinerUQuotaError,
18
+ MinerUTimeoutError,
19
+ MinerUTransientError,
20
+ )
21
+ from .models import (
22
+ MinerUProgress,
23
+ MinerUTaskResult,
24
+ MinerUTaskState,
25
+ MinerUTaskStatus,
26
+ )
27
+ from .polling import DEFAULT_POLL_POLICY, PollPolicy
28
+
29
+ __all__ = [
30
+ "DEFAULT_POLL_POLICY",
31
+ "AsyncResult",
32
+ "MetricHook",
33
+ "MinerUApiClient",
34
+ "MinerUAuthenticationError",
35
+ "MinerUClient",
36
+ "MinerUConfigurationError",
37
+ "MinerUError",
38
+ "MinerUExecutionError",
39
+ "MinerUProgress",
40
+ "MinerUQuotaError",
41
+ "MinerUTaskResult",
42
+ "MinerUTaskState",
43
+ "MinerUTaskStatus",
44
+ "MinerUTimeoutError",
45
+ "MinerUTransientError",
46
+ "PollPolicy",
47
+ ]
@@ -0,0 +1,7 @@
1
+ """兼容导出:共享实现位于 :mod:`mineru_parser._telemetry`。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .._telemetry import MetricHook, Telemetry
6
+
7
+ __all__ = ["MetricHook", "Telemetry"]
@@ -0,0 +1,13 @@
1
+ """时间辅助函数。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+
8
+ def elapsed_ms(started: float) -> float:
9
+ """返回自 ``started``(``time.perf_counter()``)以来的毫秒数。"""
10
+ return round((time.perf_counter() - started) * 1000, 3)
11
+
12
+
13
+ __all__ = ["elapsed_ms"]
@@ -0,0 +1,161 @@
1
+ """配置参数校验:纯函数,不依赖网络、日志或客户端状态。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from urllib.parse import urlsplit
7
+
8
+ from .errors import MinerUConfigurationError
9
+
10
+ _HTTP_SCHEMES = frozenset({"http", "https"})
11
+
12
+
13
+ def is_finite_number(value: object) -> bool:
14
+ """拒绝 NaN / Infinity:它们能通过 ``<= 0`` 比较,却会在 sleep
15
+ 或 socket 超时处抛出未分类异常。"""
16
+ return not (isinstance(value, float) and not math.isfinite(value))
17
+
18
+
19
+ def is_absolute_http_url(value: object) -> bool:
20
+ if not isinstance(value, str) or not value.strip():
21
+ return False
22
+ try:
23
+ parsed = urlsplit(value.strip())
24
+ _ = parsed.port # 触发非法端口校验
25
+ except ValueError:
26
+ return False
27
+ return parsed.scheme.lower() in _HTTP_SCHEMES and bool(parsed.hostname)
28
+
29
+
30
+ def validate_api_key(api_key: str | None) -> str | None:
31
+ if api_key is None:
32
+ return None
33
+ if not isinstance(api_key, str) or not api_key.strip():
34
+ raise MinerUConfigurationError("MinerU API key must not be blank.")
35
+ return api_key.strip()
36
+
37
+
38
+ def validate_base_url(base_url: str) -> str:
39
+ if not isinstance(base_url, str) or not base_url.strip():
40
+ raise MinerUConfigurationError("MinerU base_url must not be empty.")
41
+ value = base_url.strip().rstrip("/")
42
+ try:
43
+ parsed = urlsplit(value)
44
+ _ = parsed.port # 触发非法端口校验
45
+ hostname = parsed.hostname
46
+ except ValueError as exc:
47
+ raise MinerUConfigurationError("MinerU base_url is not a valid URL.") from exc
48
+ if parsed.scheme.lower() not in _HTTP_SCHEMES or not hostname:
49
+ raise MinerUConfigurationError(
50
+ "MinerU base_url must be an absolute http(s) URL."
51
+ )
52
+ if parsed.query or parsed.fragment:
53
+ raise MinerUConfigurationError(
54
+ "MinerU base_url must not contain a query or fragment."
55
+ )
56
+ if any(
57
+ ch.isspace() or ord(ch) < 32 or ord(ch) == 127 or ch == "\\" for ch in hostname
58
+ ):
59
+ raise MinerUConfigurationError("MinerU base_url contains an invalid host.")
60
+ return value
61
+
62
+
63
+ def validate_request_timeout(
64
+ value: float | tuple[float, float],
65
+ ) -> float | tuple[float, float]:
66
+ if isinstance(value, bool):
67
+ raise MinerUConfigurationError("request_timeout must be positive.")
68
+ if isinstance(value, (int, float)):
69
+ if not is_finite_number(value) or value <= 0:
70
+ raise MinerUConfigurationError("request_timeout must be positive.")
71
+ return float(value)
72
+ if isinstance(value, (tuple, list)) and len(value) == 2:
73
+ connect, read = value
74
+ for item in (connect, read):
75
+ if (
76
+ isinstance(item, bool)
77
+ or not isinstance(item, (int, float))
78
+ or not is_finite_number(item)
79
+ or item <= 0
80
+ ):
81
+ raise MinerUConfigurationError(
82
+ "request_timeout tuple values must both be positive."
83
+ )
84
+ return (float(connect), float(read))
85
+ raise MinerUConfigurationError(
86
+ "request_timeout must be a positive number or a (connect, read) tuple."
87
+ )
88
+
89
+
90
+ def validate_positive(value: float, name: str) -> float:
91
+ if (
92
+ isinstance(value, bool)
93
+ or not isinstance(value, (int, float))
94
+ or not is_finite_number(value)
95
+ ):
96
+ raise MinerUConfigurationError(f"{name} must be a positive number.")
97
+ if value <= 0:
98
+ raise MinerUConfigurationError(f"{name} must be a positive number.")
99
+ return float(value)
100
+
101
+
102
+ def validate_non_negative(value: float, name: str) -> float:
103
+ if (
104
+ isinstance(value, bool)
105
+ or not isinstance(value, (int, float))
106
+ or not is_finite_number(value)
107
+ ):
108
+ raise MinerUConfigurationError(f"{name} must be zero or positive.")
109
+ if value < 0:
110
+ raise MinerUConfigurationError(f"{name} must be zero or positive.")
111
+ return float(value)
112
+
113
+
114
+ def validate_max_errors(value: int) -> int:
115
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
116
+ raise MinerUConfigurationError(
117
+ "max_consecutive_errors must be an integer >= 1."
118
+ )
119
+ return value
120
+
121
+
122
+ def validate_task_id(task_id: str) -> str:
123
+ if not isinstance(task_id, str) or not task_id.strip():
124
+ raise MinerUConfigurationError("task_id must be a non-empty string.")
125
+ return task_id.strip()
126
+
127
+
128
+ def validate_deadline_epoch(value: float) -> float:
129
+ if (
130
+ isinstance(value, bool)
131
+ or not isinstance(value, (int, float))
132
+ or not is_finite_number(value)
133
+ ):
134
+ raise MinerUConfigurationError("deadline_epoch must be a finite number.")
135
+ return float(value)
136
+
137
+
138
+ def validate_data_id(value: str | None) -> str | None:
139
+ if value is None:
140
+ return None
141
+ if not isinstance(value, str) or not value.strip():
142
+ raise MinerUConfigurationError("data_id must not be blank.")
143
+ value = value.strip()
144
+ if len(value) > 128:
145
+ raise MinerUConfigurationError("data_id must be at most 128 characters.")
146
+ return value
147
+
148
+
149
+ __all__ = [
150
+ "is_absolute_http_url",
151
+ "is_finite_number",
152
+ "validate_api_key",
153
+ "validate_base_url",
154
+ "validate_data_id",
155
+ "validate_deadline_epoch",
156
+ "validate_max_errors",
157
+ "validate_non_negative",
158
+ "validate_positive",
159
+ "validate_request_timeout",
160
+ "validate_task_id",
161
+ ]
@@ -0,0 +1,233 @@
1
+ """``MinerUApiClient``:配置、凭证与结果句柄工厂。
2
+
3
+ 客户端本身不保存任务状态,也不执行轮询;``submit`` / ``resume`` 返回
4
+ :class:`~mineru_parser._client.async_result.AsyncResult` 句柄,由句柄负责等待。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ import random
12
+ import time
13
+ from collections.abc import Callable
14
+
15
+ import requests
16
+ from pydantic import ValidationError
17
+
18
+ from ._telemetry import MetricHook, Telemetry
19
+ from ._time import elapsed_ms
20
+ from ._validation import (
21
+ validate_api_key,
22
+ validate_base_url,
23
+ validate_data_id,
24
+ validate_request_timeout,
25
+ validate_task_id,
26
+ )
27
+ from .async_result import AsyncResult
28
+ from .errors import (
29
+ MinerUAuthenticationError,
30
+ MinerUConfigurationError,
31
+ MinerUError,
32
+ )
33
+ from .models import (
34
+ ExtractionRequest,
35
+ MinerUTaskStatus,
36
+ normalize_status,
37
+ )
38
+ from .polling import (
39
+ DEFAULT_POLL_POLICY,
40
+ Poller,
41
+ PollPolicy,
42
+ PollRuntime,
43
+ )
44
+ from .transport import MINERU_BASE_URL, MinerUTransport
45
+
46
+
47
+ class MinerUApiClient:
48
+ """MinerU API 的同步客户端。
49
+
50
+ 客户端可以在多个任务之间共享。客户端自行创建并管理底层
51
+ ``requests.Session``(复用连接池);也可以注入外部 Session,此时其
52
+ 生命周期由调用方负责,``close()`` 不会关闭它。
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ *,
58
+ api_key: str | None = None,
59
+ base_url: str = MINERU_BASE_URL,
60
+ request_timeout: float | tuple[float, float] = 10.0,
61
+ poll_policy: PollPolicy | None = None,
62
+ session: requests.Session | None = None,
63
+ sleep: Callable[[float], None] = time.sleep,
64
+ monotonic: Callable[[], float] = time.monotonic,
65
+ wall_clock: Callable[[], float] = time.time,
66
+ random_fn: Callable[[], float] = random.random,
67
+ on_metric: MetricHook | None = None,
68
+ correlation_id: str | None = None,
69
+ ) -> None:
70
+ self._api_key = validate_api_key(api_key)
71
+ self._base_url = validate_base_url(base_url)
72
+ self._request_timeout = validate_request_timeout(request_timeout)
73
+ if poll_policy is None:
74
+ poll_policy = DEFAULT_POLL_POLICY
75
+ if not isinstance(poll_policy, PollPolicy):
76
+ raise MinerUConfigurationError("poll_policy must be a PollPolicy instance.")
77
+ self._poll_policy = poll_policy
78
+ self._owns_session = session is None
79
+ self._session = session if session is not None else requests.Session()
80
+ self._runtime = PollRuntime(
81
+ sleep=sleep,
82
+ monotonic=monotonic,
83
+ wall_clock=wall_clock,
84
+ random_fn=random_fn,
85
+ )
86
+ self._telemetry = Telemetry(
87
+ on_metric=on_metric,
88
+ correlation_id=correlation_id,
89
+ )
90
+ self._transport = MinerUTransport(
91
+ base_url=self._base_url,
92
+ timeout=self._request_timeout,
93
+ session=self._session,
94
+ telemetry=self._telemetry,
95
+ wall_clock=wall_clock,
96
+ )
97
+
98
+ # --- 生命周期 ---------------------------------------------------------
99
+
100
+ def close(self) -> None:
101
+ """关闭客户端自建的 Session;注入的 Session 由调用方管理。"""
102
+ if self._owns_session:
103
+ self._session.close()
104
+
105
+ def __enter__(self) -> MinerUApiClient:
106
+ return self
107
+
108
+ def __exit__(self, *exc_info: object) -> None:
109
+ self.close()
110
+
111
+ # --- 只读配置 ---------------------------------------------------------
112
+
113
+ @property
114
+ def base_url(self) -> str:
115
+ return self._base_url
116
+
117
+ @property
118
+ def poll_policy(self) -> PollPolicy:
119
+ return self._poll_policy
120
+
121
+ # --- 提交 / 查询 ------------------------------------------------------
122
+
123
+ def submit(
124
+ self,
125
+ uri: str,
126
+ *,
127
+ model_version: str = "vlm",
128
+ data_id: str | None = None,
129
+ ) -> AsyncResult:
130
+ """提交任务,返回可等待的结果句柄。
131
+
132
+ 只有“确定请求未发出”的错误或 HTTP 429 抛
133
+ :class:`MinerUTransientError`;其余错误(包括结果未知、HTTP 4xx/5xx)
134
+ 抛 :class:`MinerUExecutionError`。``data_id`` 仅用于对账,不是幂等键。
135
+ """
136
+ api_key = self._resolve_api_key()
137
+
138
+ # 请求模型校验必须在 HTTP 异常处理块之外:Pydantic ValidationError
139
+ # 不能被误映射为 MinerUExecutionError。
140
+ try:
141
+ request = ExtractionRequest(
142
+ uri=uri,
143
+ model_version=model_version,
144
+ data_id=data_id,
145
+ )
146
+ except ValidationError as exc:
147
+ raise MinerUConfigurationError(
148
+ f"Invalid MinerU submit request: {exc}"
149
+ ) from exc
150
+
151
+ started = time.perf_counter()
152
+ try:
153
+ provider_task_id = self._transport.submit(request, api_key=api_key)
154
+ except MinerUError:
155
+ duration_ms = elapsed_ms(started)
156
+ self._telemetry.log(
157
+ "mineru.submit",
158
+ level=logging.WARNING,
159
+ data_id=request.data_id,
160
+ duration_ms=duration_ms,
161
+ outcome="error",
162
+ )
163
+ self._telemetry.metric("mineru.submit.total", 1, {"outcome": "error"})
164
+ self._telemetry.metric(
165
+ "mineru.submit.duration_ms",
166
+ duration_ms,
167
+ {"outcome": "error"},
168
+ )
169
+ raise
170
+
171
+ duration_ms = elapsed_ms(started)
172
+ self._telemetry.log(
173
+ "mineru.submit",
174
+ provider_task_id=provider_task_id,
175
+ data_id=request.data_id,
176
+ duration_ms=duration_ms,
177
+ outcome="success",
178
+ )
179
+ self._telemetry.metric("mineru.submit.total", 1, {"outcome": "success"})
180
+ self._telemetry.metric(
181
+ "mineru.submit.duration_ms",
182
+ duration_ms,
183
+ {"outcome": "success"},
184
+ )
185
+ return AsyncResult(
186
+ client=self,
187
+ task_id=provider_task_id,
188
+ data_id=request.data_id,
189
+ )
190
+
191
+ def resume(
192
+ self,
193
+ task_id: str,
194
+ *,
195
+ data_id: str | None = None,
196
+ ) -> AsyncResult:
197
+ """从已持久化的 task_id 恢复结果句柄,不发起网络请求。"""
198
+ return AsyncResult(
199
+ client=self,
200
+ task_id=validate_task_id(task_id),
201
+ data_id=validate_data_id(data_id),
202
+ )
203
+
204
+ # --- 内部 -------------------------------------------------------------
205
+
206
+ def _trace(self, task_id: str) -> MinerUTaskStatus:
207
+ """内部状态查询:仅供 ``AsyncResult.refresh()`` 调用。"""
208
+ validated = validate_task_id(task_id)
209
+ api_key = self._resolve_api_key()
210
+ detail = self._transport.trace(validated, api_key=api_key)
211
+ return normalize_status(validated, detail)
212
+
213
+ def _resolve_api_key(self) -> str:
214
+ key = self._api_key
215
+ if key is None:
216
+ key = os.getenv("MINERU_API_KEY")
217
+ if key is None or not key.strip():
218
+ raise MinerUAuthenticationError(
219
+ "MinerU API key is not configured; set MINERU_API_KEY."
220
+ )
221
+ return key.strip()
222
+
223
+ def _poller(self, policy: PollPolicy | None = None) -> Poller:
224
+ return Poller(
225
+ policy=policy if policy is not None else self._poll_policy,
226
+ runtime=self._runtime,
227
+ telemetry=self._telemetry,
228
+ )
229
+
230
+
231
+ MinerUClient = MinerUApiClient
232
+
233
+ __all__ = ["MetricHook", "MinerUApiClient", "MinerUClient"]