docugraph 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.
- docugraph/__init__.py +24 -0
- docugraph/_version.py +28 -0
- docugraph/agents/__init__.py +27 -0
- docugraph/agents/coding_agent.py +532 -0
- docugraph/agents/tools/__init__.py +62 -0
- docugraph/agents/tools/graph_query.py +203 -0
- docugraph/agents/tools/memory_ops.py +305 -0
- docugraph/agents/tools/search_code.py +305 -0
- docugraph/agents/tools/search_docs.py +148 -0
- docugraph/core/__init__.py +34 -0
- docugraph/core/config.py +183 -0
- docugraph/core/embeddings.py +394 -0
- docugraph/core/llm.py +892 -0
- docugraph/core/models.py +145 -0
- docugraph/ingestion/__init__.py +64 -0
- docugraph/ingestion/api_docs.py +646 -0
- docugraph/ingestion/chunker.py +382 -0
- docugraph/ingestion/crawler.py +305 -0
- docugraph/ingestion/git_indexer.py +556 -0
- docugraph/ingestion/local_files.py +516 -0
- docugraph/interfaces/__init__.py +1 -0
- docugraph/interfaces/cli.py +983 -0
- docugraph/interfaces/mcp_server.py +918 -0
- docugraph/retrieval/__init__.py +54 -0
- docugraph/retrieval/context_builder.py +522 -0
- docugraph/retrieval/hybrid_search.py +534 -0
- docugraph/retrieval/reranker.py +532 -0
- docugraph/storage/__init__.py +14 -0
- docugraph/storage/graph_store.py +602 -0
- docugraph/storage/memory_store.py +529 -0
- docugraph/storage/vector_store.py +367 -0
- docugraph-0.1.0.dist-info/METADATA +354 -0
- docugraph-0.1.0.dist-info/RECORD +37 -0
- docugraph-0.1.0.dist-info/WHEEL +4 -0
- docugraph-0.1.0.dist-info/entry_points.txt +3 -0
- docugraph-0.1.0.dist-info/licenses/LICENSE +201 -0
- docugraph-0.1.0.dist-info/licenses/NOTICE +10 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"""API documentation parsing for OpenAPI and GraphQL schemas.
|
|
2
|
+
|
|
3
|
+
Extracts structured information from API specifications:
|
|
4
|
+
- Endpoints, methods, parameters
|
|
5
|
+
- Types, schemas, models
|
|
6
|
+
- Descriptions and examples
|
|
7
|
+
|
|
8
|
+
Supports:
|
|
9
|
+
- OpenAPI 3.x (JSON/YAML)
|
|
10
|
+
- GraphQL SDL schemas
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from enum import StrEnum
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from docugraph.core.models import Document
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIDocType(StrEnum):
|
|
26
|
+
"""Type of API documentation."""
|
|
27
|
+
|
|
28
|
+
OPENAPI = "openapi"
|
|
29
|
+
GRAPHQL = "graphql"
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class APIParameter:
|
|
35
|
+
"""An API parameter (query, path, header, body)."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
location: str # query, path, header, body
|
|
39
|
+
param_type: str # string, integer, boolean, etc.
|
|
40
|
+
required: bool = False
|
|
41
|
+
description: str | None = None
|
|
42
|
+
default: Any = None
|
|
43
|
+
example: Any = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class APIEndpoint:
|
|
48
|
+
"""An API endpoint with its metadata."""
|
|
49
|
+
|
|
50
|
+
path: str
|
|
51
|
+
method: str # GET, POST, PUT, DELETE, etc.
|
|
52
|
+
summary: str | None = None
|
|
53
|
+
description: str | None = None
|
|
54
|
+
parameters: list[APIParameter] = field(default_factory=list)
|
|
55
|
+
request_body: dict[str, Any] | None = None
|
|
56
|
+
responses: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
tags: list[str] = field(default_factory=list)
|
|
58
|
+
deprecated: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class APISchema:
|
|
63
|
+
"""A data schema/model from the API spec."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
schema_type: str # object, array, string, etc.
|
|
67
|
+
description: str | None = None
|
|
68
|
+
properties: dict[str, Any] = field(default_factory=dict)
|
|
69
|
+
required_fields: list[str] = field(default_factory=list)
|
|
70
|
+
example: Any = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class GraphQLType:
|
|
75
|
+
"""A GraphQL type definition."""
|
|
76
|
+
|
|
77
|
+
name: str
|
|
78
|
+
kind: str # type, input, interface, enum, union, scalar
|
|
79
|
+
description: str | None = None
|
|
80
|
+
fields: list[dict[str, Any]] = field(default_factory=list)
|
|
81
|
+
values: list[str] | None = None # For enums
|
|
82
|
+
implements: list[str] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class ParsedAPISpec:
|
|
87
|
+
"""Parsed API specification."""
|
|
88
|
+
|
|
89
|
+
doc_type: APIDocType
|
|
90
|
+
title: str
|
|
91
|
+
version: str | None = None
|
|
92
|
+
description: str | None = None
|
|
93
|
+
base_url: str | None = None
|
|
94
|
+
endpoints: list[APIEndpoint] = field(default_factory=list)
|
|
95
|
+
schemas: list[APISchema] = field(default_factory=list)
|
|
96
|
+
graphql_types: list[GraphQLType] = field(default_factory=list)
|
|
97
|
+
raw_spec: dict[str, Any] | str = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class APIDocConfig:
|
|
102
|
+
"""Configuration for API documentation parsing."""
|
|
103
|
+
|
|
104
|
+
include_examples: bool = True
|
|
105
|
+
include_deprecated: bool = False
|
|
106
|
+
generate_markdown: bool = True
|
|
107
|
+
max_description_length: int = 2000
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class APIDocParser:
|
|
111
|
+
"""Parser for API documentation (OpenAPI, GraphQL).
|
|
112
|
+
|
|
113
|
+
Extracts structured information and converts to indexable documents.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, config: APIDocConfig | None = None) -> None:
|
|
117
|
+
"""Initialize the API doc parser.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
config: Parser configuration
|
|
121
|
+
"""
|
|
122
|
+
self._config = config or APIDocConfig()
|
|
123
|
+
|
|
124
|
+
def detect_type(self, content: str) -> APIDocType:
|
|
125
|
+
"""Detect the type of API documentation.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
content: Raw content string
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Detected API documentation type
|
|
132
|
+
"""
|
|
133
|
+
# Check for OpenAPI markers
|
|
134
|
+
if '"openapi"' in content or "openapi:" in content:
|
|
135
|
+
return APIDocType.OPENAPI
|
|
136
|
+
if '"swagger"' in content or "swagger:" in content:
|
|
137
|
+
return APIDocType.OPENAPI
|
|
138
|
+
|
|
139
|
+
# Check for GraphQL SDL markers
|
|
140
|
+
if re.search(r"^\s*(type|schema|query|mutation|subscription)\s+\w+", content, re.MULTILINE):
|
|
141
|
+
return APIDocType.GRAPHQL
|
|
142
|
+
|
|
143
|
+
return APIDocType.UNKNOWN
|
|
144
|
+
|
|
145
|
+
def parse(self, content: str, source_path: str | None = None) -> ParsedAPISpec:
|
|
146
|
+
"""Parse API documentation content.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
content: Raw content (JSON, YAML, or GraphQL SDL)
|
|
150
|
+
source_path: Optional source file path
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Parsed API specification
|
|
154
|
+
"""
|
|
155
|
+
doc_type = self.detect_type(content)
|
|
156
|
+
|
|
157
|
+
if doc_type == APIDocType.OPENAPI:
|
|
158
|
+
return self._parse_openapi(content, source_path)
|
|
159
|
+
elif doc_type == APIDocType.GRAPHQL:
|
|
160
|
+
return self._parse_graphql(content, source_path)
|
|
161
|
+
else:
|
|
162
|
+
return ParsedAPISpec(
|
|
163
|
+
doc_type=APIDocType.UNKNOWN,
|
|
164
|
+
title="Unknown API",
|
|
165
|
+
raw_spec=content,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def parse_file(self, file_path: str | Path) -> ParsedAPISpec:
|
|
169
|
+
"""Parse API documentation from a file.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
file_path: Path to the API spec file
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Parsed API specification
|
|
176
|
+
"""
|
|
177
|
+
path = Path(file_path)
|
|
178
|
+
content = path.read_text()
|
|
179
|
+
return self.parse(content, str(path))
|
|
180
|
+
|
|
181
|
+
def _parse_openapi(self, content: str, source_path: str | None = None) -> ParsedAPISpec:
|
|
182
|
+
"""Parse OpenAPI specification.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
content: JSON or YAML content
|
|
186
|
+
source_path: Source file path
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Parsed OpenAPI spec
|
|
190
|
+
"""
|
|
191
|
+
# Try JSON first, then YAML
|
|
192
|
+
try:
|
|
193
|
+
spec = json.loads(content)
|
|
194
|
+
except json.JSONDecodeError:
|
|
195
|
+
try:
|
|
196
|
+
import yaml
|
|
197
|
+
|
|
198
|
+
spec = yaml.safe_load(content)
|
|
199
|
+
except Exception:
|
|
200
|
+
# Fallback: try to extract basic info with regex
|
|
201
|
+
return self._parse_openapi_fallback(content, source_path)
|
|
202
|
+
|
|
203
|
+
# Extract info
|
|
204
|
+
info = spec.get("info", {})
|
|
205
|
+
servers = spec.get("servers", [])
|
|
206
|
+
base_url = servers[0].get("url") if servers else None
|
|
207
|
+
|
|
208
|
+
# Parse endpoints
|
|
209
|
+
endpoints = []
|
|
210
|
+
paths = spec.get("paths", {})
|
|
211
|
+
for path, methods in paths.items():
|
|
212
|
+
for method, details in methods.items():
|
|
213
|
+
if method.upper() not in (
|
|
214
|
+
"GET",
|
|
215
|
+
"POST",
|
|
216
|
+
"PUT",
|
|
217
|
+
"DELETE",
|
|
218
|
+
"PATCH",
|
|
219
|
+
"HEAD",
|
|
220
|
+
"OPTIONS",
|
|
221
|
+
):
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
if details.get("deprecated", False) and not self._config.include_deprecated:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# Parse parameters
|
|
228
|
+
params = []
|
|
229
|
+
for p in details.get("parameters", []):
|
|
230
|
+
params.append(
|
|
231
|
+
APIParameter(
|
|
232
|
+
name=p.get("name", ""),
|
|
233
|
+
location=p.get("in", "query"),
|
|
234
|
+
param_type=p.get("schema", {}).get("type", "string"),
|
|
235
|
+
required=p.get("required", False),
|
|
236
|
+
description=p.get("description"),
|
|
237
|
+
example=p.get("example") if self._config.include_examples else None,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
endpoints.append(
|
|
242
|
+
APIEndpoint(
|
|
243
|
+
path=path,
|
|
244
|
+
method=method.upper(),
|
|
245
|
+
summary=details.get("summary"),
|
|
246
|
+
description=self._truncate(details.get("description")),
|
|
247
|
+
parameters=params,
|
|
248
|
+
request_body=details.get("requestBody"),
|
|
249
|
+
responses=details.get("responses", {}),
|
|
250
|
+
tags=details.get("tags", []),
|
|
251
|
+
deprecated=details.get("deprecated", False),
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Parse schemas
|
|
256
|
+
schemas = []
|
|
257
|
+
components = spec.get("components", {})
|
|
258
|
+
for name, schema_def in components.get("schemas", {}).items():
|
|
259
|
+
schemas.append(
|
|
260
|
+
APISchema(
|
|
261
|
+
name=name,
|
|
262
|
+
schema_type=schema_def.get("type", "object"),
|
|
263
|
+
description=self._truncate(schema_def.get("description")),
|
|
264
|
+
properties=schema_def.get("properties", {}),
|
|
265
|
+
required_fields=schema_def.get("required", []),
|
|
266
|
+
example=schema_def.get("example") if self._config.include_examples else None,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return ParsedAPISpec(
|
|
271
|
+
doc_type=APIDocType.OPENAPI,
|
|
272
|
+
title=info.get("title", "API"),
|
|
273
|
+
version=info.get("version"),
|
|
274
|
+
description=self._truncate(info.get("description")),
|
|
275
|
+
base_url=base_url,
|
|
276
|
+
endpoints=endpoints,
|
|
277
|
+
schemas=schemas,
|
|
278
|
+
raw_spec=spec,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _parse_openapi_fallback(self, content: str, _source_path: str | None) -> ParsedAPISpec:
|
|
282
|
+
"""Fallback OpenAPI parsing using regex.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
content: Raw content
|
|
286
|
+
source_path: Source file path
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Basic parsed spec
|
|
290
|
+
"""
|
|
291
|
+
# Extract title
|
|
292
|
+
title_match = re.search(r'"title"\s*:\s*"([^"]+)"', content)
|
|
293
|
+
title = title_match.group(1) if title_match else "API"
|
|
294
|
+
|
|
295
|
+
# Extract version
|
|
296
|
+
version_match = re.search(r'"version"\s*:\s*"([^"]+)"', content)
|
|
297
|
+
version = version_match.group(1) if version_match else None
|
|
298
|
+
|
|
299
|
+
# Extract paths (basic)
|
|
300
|
+
endpoints = []
|
|
301
|
+
path_pattern = r'"(/[^"]+)"\s*:\s*\{'
|
|
302
|
+
for match in re.finditer(path_pattern, content):
|
|
303
|
+
path = match.group(1)
|
|
304
|
+
# Look for methods after this path
|
|
305
|
+
for method in ["get", "post", "put", "delete", "patch"]:
|
|
306
|
+
if f'"{method}"' in content[match.start() : match.start() + 500]:
|
|
307
|
+
endpoints.append(
|
|
308
|
+
APIEndpoint(
|
|
309
|
+
path=path,
|
|
310
|
+
method=method.upper(),
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return ParsedAPISpec(
|
|
315
|
+
doc_type=APIDocType.OPENAPI,
|
|
316
|
+
title=title,
|
|
317
|
+
version=version,
|
|
318
|
+
endpoints=endpoints,
|
|
319
|
+
raw_spec=content,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def _parse_graphql(self, content: str, _source_path: str | None = None) -> ParsedAPISpec:
|
|
323
|
+
"""Parse GraphQL SDL schema.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
content: GraphQL SDL content
|
|
327
|
+
source_path: Source file path
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Parsed GraphQL spec
|
|
331
|
+
"""
|
|
332
|
+
types: list[GraphQLType] = []
|
|
333
|
+
|
|
334
|
+
# Parse type definitions
|
|
335
|
+
type_pattern = r'(?:"""([^"]*)"""\s*)?(type|input|interface|enum|union|scalar)\s+(\w+)(?:\s+implements\s+([\w\s&]+))?(?:\s*\{([^}]*)\})?'
|
|
336
|
+
|
|
337
|
+
for match in re.finditer(type_pattern, content, re.DOTALL):
|
|
338
|
+
description = match.group(1)
|
|
339
|
+
kind = match.group(2)
|
|
340
|
+
name = match.group(3)
|
|
341
|
+
implements = match.group(4)
|
|
342
|
+
body = match.group(5)
|
|
343
|
+
|
|
344
|
+
gql_type = GraphQLType(
|
|
345
|
+
name=name,
|
|
346
|
+
kind=kind,
|
|
347
|
+
description=self._truncate(description.strip()) if description else None,
|
|
348
|
+
implements=implements.split("&") if implements else [],
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if kind == "enum" and body:
|
|
352
|
+
# Parse enum values
|
|
353
|
+
values = re.findall(r"(\w+)", body)
|
|
354
|
+
gql_type.values = values
|
|
355
|
+
elif body:
|
|
356
|
+
# Parse fields
|
|
357
|
+
field_pattern = r'(?:"""([^"]*)"""\s*)?(\w+)(?:\([^)]*\))?\s*:\s*([^\n!]+!?)'
|
|
358
|
+
for field_match in re.finditer(field_pattern, body):
|
|
359
|
+
gql_type.fields.append(
|
|
360
|
+
{
|
|
361
|
+
"description": field_match.group(1).strip()
|
|
362
|
+
if field_match.group(1)
|
|
363
|
+
else None,
|
|
364
|
+
"name": field_match.group(2),
|
|
365
|
+
"type": field_match.group(3).strip(),
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
types.append(gql_type)
|
|
370
|
+
|
|
371
|
+
# Extract schema title from comments or first type
|
|
372
|
+
title = "GraphQL API"
|
|
373
|
+
title_match = re.search(r"#\s*(.+)", content)
|
|
374
|
+
if title_match:
|
|
375
|
+
title = title_match.group(1).strip()
|
|
376
|
+
|
|
377
|
+
return ParsedAPISpec(
|
|
378
|
+
doc_type=APIDocType.GRAPHQL,
|
|
379
|
+
title=title,
|
|
380
|
+
graphql_types=types,
|
|
381
|
+
raw_spec=content,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def _truncate(self, text: str | None) -> str | None:
|
|
385
|
+
"""Truncate text to max length.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
text: Text to truncate
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Truncated text
|
|
392
|
+
"""
|
|
393
|
+
if not text:
|
|
394
|
+
return text
|
|
395
|
+
if len(text) <= self._config.max_description_length:
|
|
396
|
+
return text
|
|
397
|
+
return text[: self._config.max_description_length - 3] + "..."
|
|
398
|
+
|
|
399
|
+
def to_documents(self, spec: ParsedAPISpec, source_path: str | None = None) -> list[Document]:
|
|
400
|
+
"""Convert parsed spec to indexable documents.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
spec: Parsed API specification
|
|
404
|
+
source_path: Source file path
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
List of Document objects for indexing
|
|
408
|
+
"""
|
|
409
|
+
documents: list[Document] = []
|
|
410
|
+
base_metadata = {
|
|
411
|
+
"doc_type": spec.doc_type.value,
|
|
412
|
+
"api_title": spec.title,
|
|
413
|
+
"api_version": spec.version,
|
|
414
|
+
"source_path": source_path,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if spec.doc_type == APIDocType.OPENAPI:
|
|
418
|
+
# Create document for each endpoint
|
|
419
|
+
for endpoint in spec.endpoints:
|
|
420
|
+
content = self._endpoint_to_markdown(endpoint, spec)
|
|
421
|
+
documents.append(
|
|
422
|
+
Document(
|
|
423
|
+
content=content,
|
|
424
|
+
content_type="api_endpoint",
|
|
425
|
+
source_path=source_path,
|
|
426
|
+
metadata={
|
|
427
|
+
**base_metadata,
|
|
428
|
+
"endpoint_path": endpoint.path,
|
|
429
|
+
"endpoint_method": endpoint.method,
|
|
430
|
+
"tags": endpoint.tags,
|
|
431
|
+
},
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Create document for each schema
|
|
436
|
+
for schema in spec.schemas:
|
|
437
|
+
content = self._schema_to_markdown(schema)
|
|
438
|
+
documents.append(
|
|
439
|
+
Document(
|
|
440
|
+
content=content,
|
|
441
|
+
content_type="api_schema",
|
|
442
|
+
source_path=source_path,
|
|
443
|
+
metadata={
|
|
444
|
+
**base_metadata,
|
|
445
|
+
"schema_name": schema.name,
|
|
446
|
+
"schema_type": schema.schema_type,
|
|
447
|
+
},
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
elif spec.doc_type == APIDocType.GRAPHQL:
|
|
452
|
+
# Create document for each GraphQL type
|
|
453
|
+
for gql_type in spec.graphql_types:
|
|
454
|
+
content = self._graphql_type_to_markdown(gql_type)
|
|
455
|
+
documents.append(
|
|
456
|
+
Document(
|
|
457
|
+
content=content,
|
|
458
|
+
content_type="graphql_type",
|
|
459
|
+
source_path=source_path,
|
|
460
|
+
metadata={
|
|
461
|
+
**base_metadata,
|
|
462
|
+
"type_name": gql_type.name,
|
|
463
|
+
"type_kind": gql_type.kind,
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Add overview document
|
|
469
|
+
if spec.description or spec.endpoints or spec.graphql_types:
|
|
470
|
+
overview = self._spec_to_overview(spec)
|
|
471
|
+
documents.insert(
|
|
472
|
+
0,
|
|
473
|
+
Document(
|
|
474
|
+
content=overview,
|
|
475
|
+
content_type="api_overview",
|
|
476
|
+
source_path=source_path,
|
|
477
|
+
metadata={
|
|
478
|
+
**base_metadata,
|
|
479
|
+
"is_overview": True,
|
|
480
|
+
},
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return documents
|
|
485
|
+
|
|
486
|
+
def _endpoint_to_markdown(self, endpoint: APIEndpoint, _spec: ParsedAPISpec) -> str:
|
|
487
|
+
"""Convert endpoint to markdown.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
endpoint: API endpoint
|
|
491
|
+
spec: Full spec for context
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Markdown representation
|
|
495
|
+
"""
|
|
496
|
+
parts = [f"## {endpoint.method} {endpoint.path}"]
|
|
497
|
+
|
|
498
|
+
if endpoint.summary:
|
|
499
|
+
parts.append(f"\n{endpoint.summary}")
|
|
500
|
+
|
|
501
|
+
if endpoint.description:
|
|
502
|
+
parts.append(f"\n{endpoint.description}")
|
|
503
|
+
|
|
504
|
+
if endpoint.tags:
|
|
505
|
+
parts.append(f"\n**Tags:** {', '.join(endpoint.tags)}")
|
|
506
|
+
|
|
507
|
+
if endpoint.deprecated:
|
|
508
|
+
parts.append("\n**DEPRECATED**")
|
|
509
|
+
|
|
510
|
+
if endpoint.parameters:
|
|
511
|
+
parts.append("\n### Parameters")
|
|
512
|
+
for param in endpoint.parameters:
|
|
513
|
+
required = " (required)" if param.required else ""
|
|
514
|
+
desc = f" - {param.description}" if param.description else ""
|
|
515
|
+
parts.append(
|
|
516
|
+
f"- `{param.name}` ({param.location}, {param.param_type}){required}{desc}"
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if endpoint.request_body:
|
|
520
|
+
parts.append("\n### Request Body")
|
|
521
|
+
content = endpoint.request_body.get("content", {})
|
|
522
|
+
for media_type, details in content.items():
|
|
523
|
+
parts.append(f"**Content-Type:** {media_type}")
|
|
524
|
+
if "schema" in details:
|
|
525
|
+
schema_ref = details["schema"].get("$ref", "")
|
|
526
|
+
if schema_ref:
|
|
527
|
+
schema_name = schema_ref.split("/")[-1]
|
|
528
|
+
parts.append(f"**Schema:** {schema_name}")
|
|
529
|
+
|
|
530
|
+
if endpoint.responses:
|
|
531
|
+
parts.append("\n### Responses")
|
|
532
|
+
for status, details in endpoint.responses.items():
|
|
533
|
+
desc = details.get("description", "") if isinstance(details, dict) else ""
|
|
534
|
+
parts.append(f"- **{status}**: {desc}")
|
|
535
|
+
|
|
536
|
+
return "\n".join(parts)
|
|
537
|
+
|
|
538
|
+
def _schema_to_markdown(self, schema: APISchema) -> str:
|
|
539
|
+
"""Convert schema to markdown.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
schema: API schema
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Markdown representation
|
|
546
|
+
"""
|
|
547
|
+
parts = [f"## Schema: {schema.name}"]
|
|
548
|
+
|
|
549
|
+
if schema.description:
|
|
550
|
+
parts.append(f"\n{schema.description}")
|
|
551
|
+
|
|
552
|
+
parts.append(f"\n**Type:** {schema.schema_type}")
|
|
553
|
+
|
|
554
|
+
if schema.properties:
|
|
555
|
+
parts.append("\n### Properties")
|
|
556
|
+
for prop_name, prop_details in schema.properties.items():
|
|
557
|
+
required = " (required)" if prop_name in schema.required_fields else ""
|
|
558
|
+
prop_type = (
|
|
559
|
+
prop_details.get("type", "any") if isinstance(prop_details, dict) else "any"
|
|
560
|
+
)
|
|
561
|
+
desc = prop_details.get("description", "") if isinstance(prop_details, dict) else ""
|
|
562
|
+
parts.append(f"- `{prop_name}` ({prop_type}){required}: {desc}")
|
|
563
|
+
|
|
564
|
+
if schema.example and self._config.include_examples:
|
|
565
|
+
parts.append("\n### Example")
|
|
566
|
+
parts.append(f"```json\n{json.dumps(schema.example, indent=2)}\n```")
|
|
567
|
+
|
|
568
|
+
return "\n".join(parts)
|
|
569
|
+
|
|
570
|
+
def _graphql_type_to_markdown(self, gql_type: GraphQLType) -> str:
|
|
571
|
+
"""Convert GraphQL type to markdown.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
gql_type: GraphQL type
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Markdown representation
|
|
578
|
+
"""
|
|
579
|
+
parts = [f"## {gql_type.kind.title()}: {gql_type.name}"]
|
|
580
|
+
|
|
581
|
+
if gql_type.description:
|
|
582
|
+
parts.append(f"\n{gql_type.description}")
|
|
583
|
+
|
|
584
|
+
if gql_type.implements:
|
|
585
|
+
parts.append(f"\n**Implements:** {', '.join(gql_type.implements)}")
|
|
586
|
+
|
|
587
|
+
if gql_type.kind == "enum" and gql_type.values:
|
|
588
|
+
parts.append("\n### Values")
|
|
589
|
+
for value in gql_type.values:
|
|
590
|
+
parts.append(f"- `{value}`")
|
|
591
|
+
elif gql_type.fields:
|
|
592
|
+
parts.append("\n### Fields")
|
|
593
|
+
for field in gql_type.fields:
|
|
594
|
+
desc = f" - {field['description']}" if field.get("description") else ""
|
|
595
|
+
parts.append(f"- `{field['name']}`: {field['type']}{desc}")
|
|
596
|
+
|
|
597
|
+
return "\n".join(parts)
|
|
598
|
+
|
|
599
|
+
def _spec_to_overview(self, spec: ParsedAPISpec) -> str:
|
|
600
|
+
"""Create overview document from spec.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
spec: Parsed API spec
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Overview markdown
|
|
607
|
+
"""
|
|
608
|
+
parts = [f"# {spec.title}"]
|
|
609
|
+
|
|
610
|
+
if spec.version:
|
|
611
|
+
parts.append(f"\n**Version:** {spec.version}")
|
|
612
|
+
|
|
613
|
+
if spec.description:
|
|
614
|
+
parts.append(f"\n{spec.description}")
|
|
615
|
+
|
|
616
|
+
if spec.base_url:
|
|
617
|
+
parts.append(f"\n**Base URL:** {spec.base_url}")
|
|
618
|
+
|
|
619
|
+
if spec.endpoints:
|
|
620
|
+
parts.append("\n## Endpoints")
|
|
621
|
+
for endpoint in spec.endpoints[:20]: # Limit for overview
|
|
622
|
+
parts.append(f"- `{endpoint.method} {endpoint.path}`: {endpoint.summary or ''}")
|
|
623
|
+
|
|
624
|
+
if spec.schemas:
|
|
625
|
+
parts.append("\n## Schemas")
|
|
626
|
+
for schema in spec.schemas[:20]:
|
|
627
|
+
parts.append(f"- `{schema.name}` ({schema.schema_type})")
|
|
628
|
+
|
|
629
|
+
if spec.graphql_types:
|
|
630
|
+
parts.append("\n## Types")
|
|
631
|
+
for gql_type in spec.graphql_types[:20]:
|
|
632
|
+
parts.append(f"- `{gql_type.name}` ({gql_type.kind})")
|
|
633
|
+
|
|
634
|
+
return "\n".join(parts)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def get_api_doc_parser(config: APIDocConfig | None = None) -> APIDocParser:
|
|
638
|
+
"""Factory function to get an APIDocParser instance.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
config: Optional parser configuration
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
APIDocParser instance
|
|
645
|
+
"""
|
|
646
|
+
return APIDocParser(config=config)
|