ailang-parse 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.
- ailang_parse/__init__.py +38 -0
- ailang_parse/client.py +96 -0
- ailang_parse/compat.py +82 -0
- ailang_parse/keys.py +49 -0
- ailang_parse/py.typed +0 -0
- ailang_parse/types.py +320 -0
- ailang_parse-0.1.0.dist-info/METADATA +199 -0
- ailang_parse-0.1.0.dist-info/RECORD +9 -0
- ailang_parse-0.1.0.dist-info/WHEEL +4 -0
ailang_parse/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""AILANG Parse — Python client for the AILANG Parse document parsing API.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from ailang_parse import DocParse
|
|
6
|
+
|
|
7
|
+
client = DocParse(api_key="dp_a1b2c3d4...")
|
|
8
|
+
result = client.parse("report.docx")
|
|
9
|
+
print(result.blocks)
|
|
10
|
+
|
|
11
|
+
Unstructured migration::
|
|
12
|
+
|
|
13
|
+
from ailang_parse import UnstructuredClient
|
|
14
|
+
client = UnstructuredClient(
|
|
15
|
+
server_url="https://ailang-dev-docparse-api-ejjw6zt3bq-ew.a.run.app"
|
|
16
|
+
)
|
|
17
|
+
elements = client.general.partition(file="report.docx")
|
|
18
|
+
"""
|
|
19
|
+
from .client import DocParse
|
|
20
|
+
from .compat import UnstructuredClient
|
|
21
|
+
from .types import (
|
|
22
|
+
Block, Cell, ParseResult, DocMetadata, Summary,
|
|
23
|
+
HealthResult, FormatsResult,
|
|
24
|
+
KeyInfo, Quota, Usage, UsageInfo,
|
|
25
|
+
Element, ElementMetadata,
|
|
26
|
+
DocParseError, AuthError, QuotaError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
__all__ = [
|
|
31
|
+
"DocParse",
|
|
32
|
+
"UnstructuredClient",
|
|
33
|
+
"Block", "Cell", "ParseResult", "DocMetadata", "Summary",
|
|
34
|
+
"HealthResult", "FormatsResult",
|
|
35
|
+
"KeyInfo", "Quota", "Usage", "UsageInfo",
|
|
36
|
+
"Element", "ElementMetadata",
|
|
37
|
+
"DocParseError", "AuthError", "QuotaError",
|
|
38
|
+
]
|
ailang_parse/client.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""AILANG Parse HTTP client — handles API communication and response unwrapping."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
DocParseError, AuthError, QuotaError,
|
|
10
|
+
ParseResult, HealthResult, FormatsResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://ailang-dev-docparse-api-ejjw6zt3bq-ew.a.run.app"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DocParse:
|
|
17
|
+
"""AILANG Parse API client.
|
|
18
|
+
|
|
19
|
+
Usage::
|
|
20
|
+
|
|
21
|
+
from ailang_parse import DocParse
|
|
22
|
+
|
|
23
|
+
client = DocParse(api_key="dp_a1b2c3d4...")
|
|
24
|
+
result = client.parse("report.docx")
|
|
25
|
+
print(result.blocks)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str = "",
|
|
31
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
32
|
+
timeout: int = 60,
|
|
33
|
+
):
|
|
34
|
+
self.api_key = api_key
|
|
35
|
+
self.base_url = base_url.rstrip("/")
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
self._session = requests.Session()
|
|
38
|
+
if api_key:
|
|
39
|
+
self._session.headers["x-api-key"] = api_key
|
|
40
|
+
|
|
41
|
+
from .keys import KeyManager
|
|
42
|
+
self.keys = KeyManager(self)
|
|
43
|
+
|
|
44
|
+
# ── Core API methods ──
|
|
45
|
+
|
|
46
|
+
def parse(self, filepath: str, output_format: str = "blocks") -> ParseResult:
|
|
47
|
+
"""Parse a document file. Returns structured blocks."""
|
|
48
|
+
data = self._call("POST", "/api/v1/parse", args=[filepath, output_format])
|
|
49
|
+
return ParseResult.from_dict(data)
|
|
50
|
+
|
|
51
|
+
def health(self) -> HealthResult:
|
|
52
|
+
"""Check API health."""
|
|
53
|
+
data = self._call("GET", "/api/v1/health")
|
|
54
|
+
return HealthResult.from_dict(data)
|
|
55
|
+
|
|
56
|
+
def formats(self) -> FormatsResult:
|
|
57
|
+
"""List supported formats."""
|
|
58
|
+
data = self._call("GET", "/api/v1/formats")
|
|
59
|
+
return FormatsResult.from_dict(data)
|
|
60
|
+
|
|
61
|
+
# ── HTTP layer ──
|
|
62
|
+
|
|
63
|
+
def _call(self, method: str, path: str, args: Optional[list] = None) -> Dict[str, Any]:
|
|
64
|
+
"""Make an API call and unwrap the serve-api response envelope."""
|
|
65
|
+
url = self.base_url + path
|
|
66
|
+
|
|
67
|
+
if method == "GET":
|
|
68
|
+
resp = self._session.get(url, timeout=self.timeout)
|
|
69
|
+
else:
|
|
70
|
+
body = {"args": args} if args else {}
|
|
71
|
+
resp = self._session.post(
|
|
72
|
+
url, json=body, timeout=self.timeout
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if resp.status_code == 401:
|
|
76
|
+
raise AuthError("Invalid or missing API key", 401)
|
|
77
|
+
if resp.status_code == 429:
|
|
78
|
+
raise QuotaError("Quota exceeded", status_code=429)
|
|
79
|
+
if resp.status_code >= 400:
|
|
80
|
+
raise DocParseError(f"API error: {resp.status_code} {resp.text}", resp.status_code)
|
|
81
|
+
|
|
82
|
+
outer = resp.json()
|
|
83
|
+
|
|
84
|
+
# serve-api wraps responses: {"result": "<json string>", "module": ..., "elapsed_ms": N}
|
|
85
|
+
if "error" in outer and outer["error"]:
|
|
86
|
+
raise DocParseError(outer["error"])
|
|
87
|
+
|
|
88
|
+
result_str = outer.get("result", "")
|
|
89
|
+
if not result_str:
|
|
90
|
+
return outer
|
|
91
|
+
|
|
92
|
+
# Inner result is a JSON string that needs a second parse
|
|
93
|
+
try:
|
|
94
|
+
return json.loads(result_str)
|
|
95
|
+
except (json.JSONDecodeError, TypeError):
|
|
96
|
+
return {"raw": result_str}
|
ailang_parse/compat.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Unstructured API compatibility — drop-in replacement for unstructured-client."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .types import Element, DocParseError
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_URL = "https://ailang-dev-docparse-api-ejjw6zt3bq-ew.a.run.app"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _GeneralApi:
|
|
14
|
+
"""Mimics unstructured_client.general.GeneralApi."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, session: requests.Session, base_url: str, timeout: int):
|
|
17
|
+
self._session = session
|
|
18
|
+
self._base_url = base_url
|
|
19
|
+
self._timeout = timeout
|
|
20
|
+
|
|
21
|
+
def partition(self, file: str = "", strategy: str = "auto", **kwargs) -> List[Element]:
|
|
22
|
+
"""Partition a document — returns Unstructured-format elements.
|
|
23
|
+
|
|
24
|
+
Usage (identical to unstructured-client)::
|
|
25
|
+
|
|
26
|
+
elements = client.general.partition(file="report.docx")
|
|
27
|
+
"""
|
|
28
|
+
url = f"{self._base_url}/general/v0/general"
|
|
29
|
+
resp = self._session.post(
|
|
30
|
+
url,
|
|
31
|
+
json={"args": [file, strategy]},
|
|
32
|
+
timeout=self._timeout,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if resp.status_code >= 400:
|
|
36
|
+
raise DocParseError(f"API error: {resp.status_code}", resp.status_code)
|
|
37
|
+
|
|
38
|
+
outer = resp.json()
|
|
39
|
+
if "error" in outer and outer["error"]:
|
|
40
|
+
raise DocParseError(outer["error"])
|
|
41
|
+
|
|
42
|
+
result_str = outer.get("result", "[]")
|
|
43
|
+
try:
|
|
44
|
+
elements_raw = json.loads(result_str)
|
|
45
|
+
except (json.JSONDecodeError, TypeError):
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
if isinstance(elements_raw, list):
|
|
49
|
+
return [Element.from_dict(e) for e in elements_raw]
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UnstructuredClient:
|
|
54
|
+
"""Drop-in replacement for ``unstructured_client.UnstructuredClient``.
|
|
55
|
+
|
|
56
|
+
Migration::
|
|
57
|
+
|
|
58
|
+
# Before
|
|
59
|
+
from unstructured_client import UnstructuredClient
|
|
60
|
+
client = UnstructuredClient(server_url="https://api.unstructured.io")
|
|
61
|
+
|
|
62
|
+
# After — one import change
|
|
63
|
+
from ailang_parse import UnstructuredClient
|
|
64
|
+
client = UnstructuredClient(
|
|
65
|
+
server_url="https://ailang-dev-docparse-api-ejjw6zt3bq-ew.a.run.app"
|
|
66
|
+
)
|
|
67
|
+
# All existing code works unchanged
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
server_url: str = DEFAULT_BASE_URL,
|
|
73
|
+
api_key: str = "",
|
|
74
|
+
timeout: int = 60,
|
|
75
|
+
**kwargs, # Accept extra args for compat
|
|
76
|
+
):
|
|
77
|
+
self._session = requests.Session()
|
|
78
|
+
if api_key:
|
|
79
|
+
self._session.headers["unstructured-api-key"] = api_key
|
|
80
|
+
self._base_url = server_url.rstrip("/")
|
|
81
|
+
self._timeout = timeout
|
|
82
|
+
self.general = _GeneralApi(self._session, self._base_url, self._timeout)
|
ailang_parse/keys.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""AILANG Parse key management — generate, list, revoke, rotate, usage."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .types import KeyInfo, UsageInfo
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .client import DocParse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KeyManager:
|
|
12
|
+
"""API key management. Access via ``client.keys``."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: "DocParse"):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def generate(self, label: str = "default", user_id: str = "", auth_token: str = "") -> KeyInfo:
|
|
18
|
+
"""Generate a new API key.
|
|
19
|
+
|
|
20
|
+
.. deprecated:: 0.10.0
|
|
21
|
+
Direct key generation removed. Use the device auth flow instead:
|
|
22
|
+
1. POST /api/v1/auth/device → get device_code + user_code
|
|
23
|
+
2. POST /api/v1/auth/device/approve (with Firebase Bearer token)
|
|
24
|
+
3. POST /api/v1/auth/device/poll → get api_key
|
|
25
|
+
"""
|
|
26
|
+
raise NotImplementedError(
|
|
27
|
+
"Direct key generation removed in v0.10.0. Use device auth flow: "
|
|
28
|
+
"POST /api/v1/auth/device → approve → poll"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def list(self, user_id: str = "", auth_token: str = "") -> dict:
|
|
32
|
+
"""List API keys for a user."""
|
|
33
|
+
data = self._client._call("POST", "/api/v1/keys/list", args=[user_id])
|
|
34
|
+
return data
|
|
35
|
+
|
|
36
|
+
def revoke(self, key_id: str, user_id: str = "", auth_token: str = "") -> dict:
|
|
37
|
+
"""Revoke an API key."""
|
|
38
|
+
data = self._client._call("POST", "/api/v1/keys/revoke", args=[key_id, user_id])
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
def rotate(self, key_id: str, user_id: str = "", auth_token: str = "") -> KeyInfo:
|
|
42
|
+
"""Rotate a key — generates new key, revokes old one, preserves tier."""
|
|
43
|
+
data = self._client._call("POST", "/api/v1/keys/rotate", args=[key_id, user_id])
|
|
44
|
+
return KeyInfo.from_dict(data)
|
|
45
|
+
|
|
46
|
+
def usage(self, key_id: str, user_id: str = "", auth_token: str = "") -> UsageInfo:
|
|
47
|
+
"""Get usage statistics for a key."""
|
|
48
|
+
data = self._client._call("POST", "/api/v1/keys/usage", args=[key_id, user_id])
|
|
49
|
+
return UsageInfo.from_dict(data)
|
ailang_parse/py.typed
ADDED
|
File without changes
|
ailang_parse/types.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""AILANG Parse types — Block ADT, ParseResult, metadata, errors."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ── Errors ──
|
|
8
|
+
|
|
9
|
+
class DocParseError(Exception):
|
|
10
|
+
"""Base error for all AILANG Parse API errors."""
|
|
11
|
+
def __init__(self, message: str, status_code: int = 0):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthError(DocParseError):
|
|
17
|
+
"""Invalid or missing API key."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QuotaError(DocParseError):
|
|
22
|
+
"""Quota exceeded (daily requests or monthly pages)."""
|
|
23
|
+
def __init__(self, message: str, tier: str = "", used: int = 0, limit: int = 0):
|
|
24
|
+
super().__init__(message, 429)
|
|
25
|
+
self.tier = tier
|
|
26
|
+
self.used = used
|
|
27
|
+
self.limit = limit
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Cell (for tables) ──
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Cell:
|
|
34
|
+
text: str = ""
|
|
35
|
+
col_span: int = 1
|
|
36
|
+
merged: bool = False
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_raw(cls, raw: Any) -> "Cell":
|
|
40
|
+
if isinstance(raw, str):
|
|
41
|
+
return cls(text=raw)
|
|
42
|
+
if isinstance(raw, dict):
|
|
43
|
+
return cls(
|
|
44
|
+
text=raw.get("text", ""),
|
|
45
|
+
col_span=raw.get("colSpan", 1),
|
|
46
|
+
merged=raw.get("merged", False),
|
|
47
|
+
)
|
|
48
|
+
return cls(text=str(raw))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Block variants ──
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Block:
|
|
55
|
+
type: str = ""
|
|
56
|
+
# TextBlock / HeadingBlock / ChangeBlock
|
|
57
|
+
text: str = ""
|
|
58
|
+
level: int = 0
|
|
59
|
+
style: str = ""
|
|
60
|
+
# ChangeBlock
|
|
61
|
+
change_type: str = ""
|
|
62
|
+
author: str = ""
|
|
63
|
+
date: str = ""
|
|
64
|
+
# TableBlock
|
|
65
|
+
headers: List[Cell] = field(default_factory=list)
|
|
66
|
+
rows: List[List[Cell]] = field(default_factory=list)
|
|
67
|
+
# ListBlock
|
|
68
|
+
items: List[str] = field(default_factory=list)
|
|
69
|
+
ordered: bool = False
|
|
70
|
+
# ImageBlock / AudioBlock / VideoBlock
|
|
71
|
+
description: str = ""
|
|
72
|
+
transcription: str = ""
|
|
73
|
+
mime: str = ""
|
|
74
|
+
data_length: int = 0
|
|
75
|
+
# SectionBlock
|
|
76
|
+
kind: str = ""
|
|
77
|
+
children: List["Block"] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, d: Dict[str, Any]) -> "Block":
|
|
81
|
+
block_type = d.get("type", "")
|
|
82
|
+
b = cls(type=block_type)
|
|
83
|
+
|
|
84
|
+
b.text = d.get("text", "")
|
|
85
|
+
b.level = d.get("level", 0)
|
|
86
|
+
b.style = d.get("style", "")
|
|
87
|
+
b.change_type = d.get("changeType", "")
|
|
88
|
+
b.author = d.get("author", "")
|
|
89
|
+
b.date = d.get("date", "")
|
|
90
|
+
b.description = d.get("description", "")
|
|
91
|
+
b.transcription = d.get("transcription", "")
|
|
92
|
+
b.mime = d.get("mime", "")
|
|
93
|
+
b.data_length = d.get("dataLength", 0)
|
|
94
|
+
b.kind = d.get("kind", "")
|
|
95
|
+
b.ordered = d.get("ordered", False)
|
|
96
|
+
b.items = d.get("items", [])
|
|
97
|
+
|
|
98
|
+
# Table
|
|
99
|
+
b.headers = [Cell.from_raw(c) for c in d.get("headers", [])]
|
|
100
|
+
b.rows = [[Cell.from_raw(c) for c in row] for row in d.get("rows", [])]
|
|
101
|
+
|
|
102
|
+
# Section (recursive)
|
|
103
|
+
b.children = [Block.from_dict(child) for child in d.get("blocks", [])]
|
|
104
|
+
|
|
105
|
+
return b
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── Metadata ──
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class DocMetadata:
|
|
112
|
+
title: str = ""
|
|
113
|
+
author: str = ""
|
|
114
|
+
created: str = ""
|
|
115
|
+
modified: str = ""
|
|
116
|
+
page_count: int = 0
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, d: Dict[str, Any]) -> "DocMetadata":
|
|
120
|
+
return cls(
|
|
121
|
+
title=d.get("title", ""),
|
|
122
|
+
author=d.get("author", ""),
|
|
123
|
+
created=d.get("created", ""),
|
|
124
|
+
modified=d.get("modified", ""),
|
|
125
|
+
page_count=d.get("pageCount", 0),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class Summary:
|
|
131
|
+
total_blocks: int = 0
|
|
132
|
+
headings: int = 0
|
|
133
|
+
tables: int = 0
|
|
134
|
+
images: int = 0
|
|
135
|
+
changes: int = 0
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_dict(cls, d: Dict[str, Any]) -> "Summary":
|
|
139
|
+
return cls(
|
|
140
|
+
total_blocks=d.get("totalBlocks", 0),
|
|
141
|
+
headings=d.get("headings", 0),
|
|
142
|
+
tables=d.get("tables", 0),
|
|
143
|
+
images=d.get("images", 0),
|
|
144
|
+
changes=d.get("changes", 0),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ── Parse result ──
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class ParseResult:
|
|
152
|
+
status: str = ""
|
|
153
|
+
filename: str = ""
|
|
154
|
+
format: str = ""
|
|
155
|
+
blocks: List[Block] = field(default_factory=list)
|
|
156
|
+
metadata: DocMetadata = field(default_factory=DocMetadata)
|
|
157
|
+
summary: Summary = field(default_factory=Summary)
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ParseResult":
|
|
161
|
+
return cls(
|
|
162
|
+
status=d.get("status", ""),
|
|
163
|
+
filename=d.get("filename", ""),
|
|
164
|
+
format=d.get("format", ""),
|
|
165
|
+
blocks=[Block.from_dict(b) for b in d.get("blocks", [])],
|
|
166
|
+
metadata=DocMetadata.from_dict(d.get("metadata", {})),
|
|
167
|
+
summary=Summary.from_dict(d.get("summary", {})),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── Health / Formats ──
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class HealthResult:
|
|
175
|
+
status: str = ""
|
|
176
|
+
version: str = ""
|
|
177
|
+
service: str = ""
|
|
178
|
+
formats_parse: int = 0
|
|
179
|
+
formats_generate: int = 0
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def from_dict(cls, d: Dict[str, Any]) -> "HealthResult":
|
|
183
|
+
return cls(
|
|
184
|
+
status=d.get("status", ""),
|
|
185
|
+
version=d.get("version", ""),
|
|
186
|
+
service=d.get("service", ""),
|
|
187
|
+
formats_parse=int(d.get("formats_parse", 0)),
|
|
188
|
+
formats_generate=int(d.get("formats_generate", 0)),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class FormatsResult:
|
|
194
|
+
parse: List[str] = field(default_factory=list)
|
|
195
|
+
generate: List[str] = field(default_factory=list)
|
|
196
|
+
ai_required: List[str] = field(default_factory=list)
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def from_dict(cls, d: Dict[str, Any]) -> "FormatsResult":
|
|
200
|
+
return cls(
|
|
201
|
+
parse=d.get("parse", []),
|
|
202
|
+
generate=d.get("generate", []),
|
|
203
|
+
ai_required=d.get("ai_required", []),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ── Key management types ──
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class Quota:
|
|
211
|
+
requests_per_day: int = 0
|
|
212
|
+
pages_per_month: int = 0
|
|
213
|
+
ai_limit_per_request: int = 0
|
|
214
|
+
fs_limit_per_request: int = 0
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def from_dict(cls, d: Dict[str, Any]) -> "Quota":
|
|
218
|
+
return cls(
|
|
219
|
+
requests_per_day=int(d.get("requestsPerDay", 0)),
|
|
220
|
+
pages_per_month=int(d.get("pagesPerMonth", 0)),
|
|
221
|
+
ai_limit_per_request=int(d.get("aiLimitPerRequest", 0)),
|
|
222
|
+
fs_limit_per_request=int(d.get("fsLimitPerRequest", 0)),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class KeyInfo:
|
|
228
|
+
status: str = ""
|
|
229
|
+
key: str = ""
|
|
230
|
+
key_id: str = ""
|
|
231
|
+
label: str = ""
|
|
232
|
+
tier: str = ""
|
|
233
|
+
created: str = ""
|
|
234
|
+
quota: Quota = field(default_factory=Quota)
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def from_dict(cls, d: Dict[str, Any]) -> "KeyInfo":
|
|
238
|
+
return cls(
|
|
239
|
+
status=d.get("status", ""),
|
|
240
|
+
key=d.get("key", ""),
|
|
241
|
+
key_id=d.get("keyId", ""),
|
|
242
|
+
label=d.get("label", ""),
|
|
243
|
+
tier=d.get("tier", ""),
|
|
244
|
+
created=d.get("created", ""),
|
|
245
|
+
quota=Quota.from_dict(d.get("quota", {})),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class Usage:
|
|
251
|
+
requests_today: int = 0
|
|
252
|
+
pages_this_month: int = 0
|
|
253
|
+
total_requests: int = 0
|
|
254
|
+
total_pages: int = 0
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def from_dict(cls, d: Dict[str, Any]) -> "Usage":
|
|
258
|
+
return cls(
|
|
259
|
+
requests_today=int(d.get("requestsToday", 0)),
|
|
260
|
+
pages_this_month=int(d.get("pagesThisMonth", 0)),
|
|
261
|
+
total_requests=int(d.get("totalRequests", 0)),
|
|
262
|
+
total_pages=int(d.get("totalPages", 0)),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass
|
|
267
|
+
class UsageInfo:
|
|
268
|
+
status: str = ""
|
|
269
|
+
key_id: str = ""
|
|
270
|
+
tier: str = ""
|
|
271
|
+
usage: Usage = field(default_factory=Usage)
|
|
272
|
+
quota: Quota = field(default_factory=Quota)
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def from_dict(cls, d: Dict[str, Any]) -> "UsageInfo":
|
|
276
|
+
return cls(
|
|
277
|
+
status=d.get("status", ""),
|
|
278
|
+
key_id=d.get("keyId", ""),
|
|
279
|
+
tier=d.get("tier", ""),
|
|
280
|
+
usage=Usage.from_dict(d.get("usage", {})),
|
|
281
|
+
quota=Quota.from_dict(d.get("quota", {})),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── Unstructured compatibility ──
|
|
286
|
+
|
|
287
|
+
@dataclass
|
|
288
|
+
class ElementMetadata:
|
|
289
|
+
filename: str = ""
|
|
290
|
+
filetype: str = ""
|
|
291
|
+
category_depth: int = 0
|
|
292
|
+
image_mime_type: str = ""
|
|
293
|
+
text_as_html: str = ""
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ElementMetadata":
|
|
297
|
+
return cls(
|
|
298
|
+
filename=d.get("filename", ""),
|
|
299
|
+
filetype=d.get("filetype", ""),
|
|
300
|
+
category_depth=d.get("category_depth", 0),
|
|
301
|
+
image_mime_type=d.get("image_mime_type", ""),
|
|
302
|
+
text_as_html=d.get("text_as_html", ""),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@dataclass
|
|
307
|
+
class Element:
|
|
308
|
+
type: str = ""
|
|
309
|
+
element_id: str = ""
|
|
310
|
+
text: str = ""
|
|
311
|
+
metadata: ElementMetadata = field(default_factory=ElementMetadata)
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def from_dict(cls, d: Dict[str, Any]) -> "Element":
|
|
315
|
+
return cls(
|
|
316
|
+
type=d.get("type", ""),
|
|
317
|
+
element_id=d.get("element_id", ""),
|
|
318
|
+
text=d.get("text", ""),
|
|
319
|
+
metadata=ElementMetadata.from_dict(d.get("metadata", {})),
|
|
320
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ailang-parse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the AILANG Parse document parsing API
|
|
5
|
+
Project-URL: Homepage, https://www.sunholo.com/docparse
|
|
6
|
+
Project-URL: Repository, https://github.com/sunholo-data/docparse
|
|
7
|
+
Project-URL: Documentation, https://www.sunholo.com/docparse/api.html
|
|
8
|
+
Author-email: Sunholo <dev@sunholo.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: ailang,ailang-parse,docparse,document-parsing,office-parser,unstructured
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Text Processing
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Requires-Dist: requests>=2.20
|
|
18
|
+
Provides-Extra: async
|
|
19
|
+
Requires-Dist: httpx>=0.24; extra == 'async'
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# AILANG Parse Python SDK
|
|
25
|
+
|
|
26
|
+
Python client for the [AILANG Parse](https://www.sunholo.com/docparse) document parsing API. Parse 13 formats, generate 8 — zero dependencies for Office, pluggable AI for PDFs.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install ailang-parse
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from ailang_parse import DocParse
|
|
38
|
+
|
|
39
|
+
client = DocParse(api_key="dp_your_key_here")
|
|
40
|
+
|
|
41
|
+
# Parse a document
|
|
42
|
+
result = client.parse("report.docx")
|
|
43
|
+
print(f"{len(result.blocks)} blocks, format: {result.format}")
|
|
44
|
+
|
|
45
|
+
for block in result.blocks:
|
|
46
|
+
if block.type == "heading":
|
|
47
|
+
print(f" H{block.level}: {block.text}")
|
|
48
|
+
elif block.type == "table":
|
|
49
|
+
print(f" Table: {len(block.headers)} cols, {len(block.rows)} rows")
|
|
50
|
+
elif block.type == "change":
|
|
51
|
+
print(f" {block.change_type} by {block.author}: {block.text}")
|
|
52
|
+
else:
|
|
53
|
+
print(f" {block.type}: {block.text[:80]}")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Parse Documents
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# Parse with different output formats
|
|
60
|
+
result = client.parse("report.docx") # Block ADT (default)
|
|
61
|
+
result = client.parse("report.docx", output_format="markdown") # Markdown
|
|
62
|
+
result = client.parse("report.docx", output_format="html") # HTML
|
|
63
|
+
|
|
64
|
+
# Access structured data
|
|
65
|
+
print(result.status) # "success"
|
|
66
|
+
print(result.filename) # "report.docx"
|
|
67
|
+
print(result.format) # "zip-office"
|
|
68
|
+
print(result.blocks) # List[Block]
|
|
69
|
+
print(result.metadata.title) # Document title
|
|
70
|
+
print(result.metadata.author) # Document author
|
|
71
|
+
print(result.summary.tables) # Number of tables found
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Supported Formats
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
formats = client.formats()
|
|
78
|
+
print(formats.parse) # ['docx', 'pptx', 'xlsx', 'odt', 'odp', 'ods', 'html', 'md', 'csv', 'epub', 'pdf', 'png', 'jpg']
|
|
79
|
+
print(formats.generate) # ['docx', 'pptx', 'xlsx', 'odt', 'odp', 'ods', 'html', 'md']
|
|
80
|
+
print(formats.ai_required) # ['pdf', 'png', 'jpg', 'gif', 'bmp', 'tiff']
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Block Types
|
|
84
|
+
|
|
85
|
+
AILANG Parse returns 9 block types:
|
|
86
|
+
|
|
87
|
+
| Type | Fields | Description |
|
|
88
|
+
|------|--------|-------------|
|
|
89
|
+
| `text` | `text`, `style`, `level` | Paragraphs, code blocks |
|
|
90
|
+
| `heading` | `text`, `level` (1-6) | Document headings |
|
|
91
|
+
| `table` | `headers`, `rows` | Tables with merge tracking |
|
|
92
|
+
| `list` | `items`, `ordered` | Ordered/unordered lists |
|
|
93
|
+
| `image` | `description`, `mime`, `data_length` | Embedded images |
|
|
94
|
+
| `audio` | `transcription`, `mime` | Audio transcriptions |
|
|
95
|
+
| `video` | `description`, `mime` | Video descriptions |
|
|
96
|
+
| `section` | `kind`, `children` | Slides, sheets, headers/footers |
|
|
97
|
+
| `change` | `change_type`, `author`, `date`, `text` | Track changes |
|
|
98
|
+
|
|
99
|
+
### Table cells
|
|
100
|
+
|
|
101
|
+
Table cells can be simple strings or merged cells:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
for block in result.blocks:
|
|
105
|
+
if block.type == "table":
|
|
106
|
+
for cell in block.headers:
|
|
107
|
+
print(f" {cell.text} (colspan={cell.col_span}, merged={cell.merged})")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Nested sections
|
|
111
|
+
|
|
112
|
+
Section blocks contain child blocks (slides, sheets, headers/footers):
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
for block in result.blocks:
|
|
116
|
+
if block.type == "section":
|
|
117
|
+
print(f"Section: {block.kind}") # "slide", "sheet", "header", "footer", etc.
|
|
118
|
+
for child in block.children:
|
|
119
|
+
print(f" {child.type}: {child.text[:50]}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Key Management
|
|
123
|
+
|
|
124
|
+
Key generation uses the device auth flow (v0.10.0+). Direct generation is no longer available.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# Get a key via device auth flow:
|
|
128
|
+
# 1. POST /api/v1/auth/device → {device_code, user_code, verification_url}
|
|
129
|
+
# 2. User opens verification_url, signs in, clicks Approve
|
|
130
|
+
# 3. POST /api/v1/auth/device/poll → {api_key, tier}
|
|
131
|
+
|
|
132
|
+
# Check usage
|
|
133
|
+
usage = client.keys.usage(key_id="abc123", user_id="user123")
|
|
134
|
+
print(f"Requests today: {usage.usage.requests_today} / {usage.quota.requests_per_day}")
|
|
135
|
+
print(f"Pages this month: {usage.usage.pages_this_month} / {usage.quota.pages_per_month}")
|
|
136
|
+
|
|
137
|
+
# Rotate (new key, old one revoked, same tier)
|
|
138
|
+
new_key = client.keys.rotate(key_id="abc123", user_id="user123")
|
|
139
|
+
print(new_key.key) # New key
|
|
140
|
+
|
|
141
|
+
# Revoke
|
|
142
|
+
client.keys.revoke(key_id="abc123", user_id="user123")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Migrating from Unstructured
|
|
146
|
+
|
|
147
|
+
One import change:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Before
|
|
151
|
+
from unstructured_client import UnstructuredClient
|
|
152
|
+
client = UnstructuredClient(server_url="https://api.unstructured.io")
|
|
153
|
+
|
|
154
|
+
# After
|
|
155
|
+
from ailang_parse import UnstructuredClient
|
|
156
|
+
client = UnstructuredClient(
|
|
157
|
+
server_url="https://ailang-dev-docparse-api-ejjw6zt3bq-ew.a.run.app"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# All existing code works unchanged
|
|
161
|
+
elements = client.general.partition(file="report.docx")
|
|
162
|
+
for el in elements:
|
|
163
|
+
print(f"{el.type}: {el.text[:80]}")
|
|
164
|
+
print(f" metadata: {el.metadata.filename}")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Error Handling
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from ailang_parse import DocParse, DocParseError, AuthError, QuotaError
|
|
171
|
+
|
|
172
|
+
client = DocParse(api_key="dp_invalid")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
result = client.parse("file.docx")
|
|
176
|
+
except AuthError as e:
|
|
177
|
+
print(f"Bad key: {e}") # 401
|
|
178
|
+
except QuotaError as e:
|
|
179
|
+
print(f"Quota exceeded: {e}") # 429
|
|
180
|
+
except DocParseError as e:
|
|
181
|
+
print(f"API error ({e.status_code}): {e}")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Configuration
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
client = DocParse(
|
|
188
|
+
api_key="dp_your_key",
|
|
189
|
+
base_url="https://your-deployment.run.app", # Custom endpoint
|
|
190
|
+
timeout=120, # Request timeout (seconds)
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Links
|
|
195
|
+
|
|
196
|
+
- [AILANG Parse Website](https://www.sunholo.com/docparse)
|
|
197
|
+
- [API Documentation](https://www.sunholo.com/docparse/api.html)
|
|
198
|
+
- [GitHub](https://github.com/sunholo-data/docparse)
|
|
199
|
+
- [Swagger UI](https://ailang-dev-docparse-api-ejjw6zt3bq-ew.a.run.app/api/_meta/docs)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ailang_parse/__init__.py,sha256=wO-cRBvtje_TRbQMklxaFI4Rm2J-lxfFX3Ox2WUNzuQ,1087
|
|
2
|
+
ailang_parse/client.py,sha256=YOQjjGhFH5I6H_5aq9Qd2iAN8Oi1BMPDanGt3n2CG8k,3073
|
|
3
|
+
ailang_parse/compat.py,sha256=PUwSRtp5kIiUr2UKrVifHwTWKfhHNQFnR6a5o1AClZ4,2638
|
|
4
|
+
ailang_parse/keys.py,sha256=lq1b5FWBJYGnzZKUQVAkv4eqlD9hpfwTeW1h20w0qzU,2039
|
|
5
|
+
ailang_parse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
ailang_parse/types.py,sha256=erbSKl1g4C6Aj_NeUomdPTV3isirRUEXh1X5uNGTTTM,8820
|
|
7
|
+
ailang_parse-0.1.0.dist-info/METADATA,sha256=FClvYhwcyi5L1BzTtXPcXgFHuXIu5NcqVFpEba6cRYs,6393
|
|
8
|
+
ailang_parse-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
ailang_parse-0.1.0.dist-info/RECORD,,
|