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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any