Moibe commited on
Commit
08040eb
·
1 Parent(s): f60d029

gitignore

Browse files
Files changed (8) hide show
  1. .gitignore +5 -0
  2. Dockerfile +12 -0
  3. app/__init__.py +0 -0
  4. app/main.py +16 -0
  5. app/parser.py +140 -0
  6. app/routes.py +27 -0
  7. app/schemas.py +42 -0
  8. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .env
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
app/__init__.py ADDED
File without changes
app/main.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+
3
+ from .routes import router
4
+
5
+ app = FastAPI(
6
+ title="OpenAPI Parser",
7
+ description="Recibe una especificación OpenAPI y la transforma en información útil para visualización gráfica de endpoints.",
8
+ version="0.1.0",
9
+ )
10
+
11
+ app.include_router(router)
12
+
13
+
14
+ @app.get("/health")
15
+ async def health():
16
+ return {"status": "ok"}
app/parser.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+
3
+ from .schemas import (
4
+ EndpointInfo,
5
+ Parameter,
6
+ RequestBody,
7
+ Response,
8
+ )
9
+
10
+ JSON_TYPE_MAP = {
11
+ "string": "string",
12
+ "integer": "integer",
13
+ "number": "number",
14
+ "boolean": "boolean",
15
+ "array": "array",
16
+ "object": "object",
17
+ }
18
+
19
+
20
+ def _resolve_ref(spec: dict, ref: str) -> dict:
21
+ """Resolve a $ref pointer like '#/components/schemas/MyModel'."""
22
+ parts = ref.lstrip("#/").split("/")
23
+ node = spec
24
+ for part in parts:
25
+ node = node[part]
26
+ return node
27
+
28
+
29
+ def _get_type(schema: dict, spec: dict) -> str:
30
+ if "$ref" in schema:
31
+ schema = _resolve_ref(spec, schema["$ref"])
32
+ if "type" in schema:
33
+ return JSON_TYPE_MAP.get(schema["type"], schema["type"])
34
+ if "anyOf" in schema:
35
+ types = [_get_type(s, spec) for s in schema["anyOf"] if s.get("type") != "null"]
36
+ return types[0] if len(types) == 1 else " | ".join(types)
37
+ return "unknown"
38
+
39
+
40
+ def _extract_fields(schema: dict, spec: dict) -> dict[str, str]:
41
+ """Extract field_name -> type from an object schema."""
42
+ if "$ref" in schema:
43
+ schema = _resolve_ref(spec, schema["$ref"])
44
+ properties = schema.get("properties", {})
45
+ return {name: _get_type(prop, spec) for name, prop in properties.items()}
46
+
47
+
48
+ def _parse_parameters(params: list[dict], spec: dict) -> list[Parameter]:
49
+ result = []
50
+ for p in params:
51
+ if "$ref" in p:
52
+ p = _resolve_ref(spec, p["$ref"])
53
+ schema = p.get("schema", {})
54
+ result.append(
55
+ Parameter(
56
+ name=p["name"],
57
+ location=p["in"],
58
+ type=_get_type(schema, spec),
59
+ required=p.get("required", False),
60
+ description=p.get("description"),
61
+ )
62
+ )
63
+ return result
64
+
65
+
66
+ def _parse_request_body(body: dict | None, spec: dict) -> RequestBody | None:
67
+ if not body:
68
+ return None
69
+ if "$ref" in body:
70
+ body = _resolve_ref(spec, body["$ref"])
71
+ content = body.get("content", {})
72
+ for content_type, media in content.items():
73
+ schema = media.get("schema", {})
74
+ fields = _extract_fields(schema, spec)
75
+ return RequestBody(content_type=content_type, fields=fields)
76
+ return None
77
+
78
+
79
+ def _parse_responses(responses: dict, spec: dict) -> list[Response]:
80
+ result = []
81
+ for status_code, resp in responses.items():
82
+ if "$ref" in resp:
83
+ resp = _resolve_ref(spec, resp["$ref"])
84
+ content = resp.get("content", {})
85
+ if content:
86
+ for content_type, media in content.items():
87
+ schema = media.get("schema", {})
88
+ fields = _extract_fields(schema, spec)
89
+ result.append(
90
+ Response(
91
+ status_code=str(status_code),
92
+ description=resp.get("description"),
93
+ content_type=content_type,
94
+ fields=fields,
95
+ )
96
+ )
97
+ break
98
+ else:
99
+ result.append(
100
+ Response(
101
+ status_code=str(status_code),
102
+ description=resp.get("description"),
103
+ fields={},
104
+ )
105
+ )
106
+ return result
107
+
108
+
109
+ def parse_endpoint(spec: dict, path: str, method: str, operation: dict) -> EndpointInfo:
110
+ return EndpointInfo(
111
+ path=path,
112
+ method=method.upper(),
113
+ summary=operation.get("summary"),
114
+ description=operation.get("description"),
115
+ operation_id=operation.get("operationId"),
116
+ parameters=_parse_parameters(operation.get("parameters", []), spec),
117
+ request_body=_parse_request_body(operation.get("requestBody"), spec),
118
+ responses=_parse_responses(operation.get("responses", {}), spec),
119
+ )
120
+
121
+
122
+ async def fetch_and_parse(spec_url: str, path: str | None = None, method: str | None = None) -> list[EndpointInfo]:
123
+ async with httpx.AsyncClient() as client:
124
+ resp = await client.get(str(spec_url), follow_redirects=True)
125
+ resp.raise_for_status()
126
+ spec = resp.json()
127
+
128
+ endpoints: list[EndpointInfo] = []
129
+
130
+ for ep_path, methods in spec.get("paths", {}).items():
131
+ if path and ep_path != path:
132
+ continue
133
+ for ep_method, operation in methods.items():
134
+ if ep_method in ("parameters", "summary", "description", "servers"):
135
+ continue
136
+ if method and ep_method.upper() != method.upper():
137
+ continue
138
+ endpoints.append(parse_endpoint(spec, ep_path, ep_method, operation))
139
+
140
+ return endpoints
app/routes.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Query
2
+
3
+ from .parser import fetch_and_parse
4
+ from .schemas import ParseResponse
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/parse", response_model=ParseResponse)
10
+ async def parse_spec(
11
+ spec_url: str = Query(..., description="URL del openapi.json a parsear"),
12
+ path: str | None = Query(None, description="Path específico, ej: /whatsapp"),
13
+ method: str | None = Query(None, description="Método HTTP, ej: GET, POST"),
14
+ ):
15
+ try:
16
+ endpoints = await fetch_and_parse(
17
+ spec_url=spec_url,
18
+ path=path,
19
+ method=method,
20
+ )
21
+ except Exception as e:
22
+ raise HTTPException(status_code=400, detail=str(e))
23
+
24
+ if not endpoints:
25
+ raise HTTPException(status_code=404, detail="No matching endpoints found")
26
+
27
+ return ParseResponse(endpoints=endpoints)
app/schemas.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, HttpUrl
2
+
3
+
4
+ class ParseRequest(BaseModel):
5
+ spec_url: HttpUrl
6
+ path: str | None = None
7
+ method: str | None = None
8
+
9
+
10
+ class Parameter(BaseModel):
11
+ name: str
12
+ location: str # query, path, header, cookie
13
+ type: str
14
+ required: bool
15
+ description: str | None = None
16
+
17
+
18
+ class RequestBody(BaseModel):
19
+ content_type: str
20
+ fields: dict[str, str] # field_name -> type
21
+
22
+
23
+ class Response(BaseModel):
24
+ status_code: str
25
+ description: str | None = None
26
+ content_type: str | None = None
27
+ fields: dict[str, str] # field_name -> type
28
+
29
+
30
+ class EndpointInfo(BaseModel):
31
+ path: str
32
+ method: str
33
+ summary: str | None = None
34
+ description: str | None = None
35
+ operation_id: str | None = None
36
+ parameters: list[Parameter]
37
+ request_body: RequestBody | None = None
38
+ responses: list[Response]
39
+
40
+
41
+ class ParseResponse(BaseModel):
42
+ endpoints: list[EndpointInfo]
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastapi[standard]>=0.115.0
2
+ httpx>=0.27.0