Moibe commited on
Commit
00a7923
·
1 Parent(s): 24b7bd1

/procesador listo

Browse files
Files changed (6) hide show
  1. .gitignore +136 -0
  2. Dockerfile +15 -0
  3. main.py +53 -0
  4. requirements.txt +6 -0
  5. routers/procesador.py +143 -0
  6. schemas.py +31 -0
.gitignore ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py,cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+ db.sqlite3-journal
61
+
62
+ # Flask stuff:
63
+ instance/
64
+ .webassets-cache
65
+
66
+ # Scrapy stuff:
67
+ .scrapy
68
+
69
+ # Sphinx documentation
70
+ docs/_build/
71
+
72
+ # PyBuilder
73
+ target/
74
+
75
+ # Jupyter Notebook
76
+ .ipynb_checkpoints
77
+
78
+ # IPython
79
+ profile_default/
80
+ ipython_config.py
81
+
82
+ # pyenv
83
+ .python-version
84
+
85
+ # pipenv
86
+ Pipfile.lock
87
+
88
+ # PEP 582
89
+ __pypackages__/
90
+
91
+ # Celery stuff
92
+ celerybeat-schedule
93
+ celerybeat.pid
94
+
95
+ # SageMath parsed files
96
+ *.sage.py
97
+
98
+ # Environments
99
+ .env
100
+ .venv
101
+ env/
102
+ venv/
103
+ ENV/
104
+ env.bak/
105
+ venv.bak/
106
+
107
+ # Spyder project settings
108
+ .spyderproject
109
+ .spyproject
110
+
111
+ # Rope project settings
112
+ .ropeproject
113
+
114
+ # mkdocs documentation
115
+ /site
116
+
117
+ # mypy
118
+ .mypy_cache/
119
+ .dmypy.json
120
+ dmypy.json
121
+
122
+ # Pyre type checker
123
+ .pyre/
124
+
125
+ # IDE
126
+ .vscode/
127
+ .idea/
128
+ *.swp
129
+ *.swo
130
+ *~
131
+ .DS_Store
132
+
133
+ # Project specific
134
+ *.db
135
+ tmp/
136
+ logs/
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ FROM python:3.11-slim
3
+
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ WORKDIR /app
9
+
10
+ COPY --chown=user ./requirements.txt requirements.txt
11
+ RUN pip install --no-cache-dir --upgrade pip && \
12
+ pip install --no-cache-dir --upgrade -r requirements.txt
13
+
14
+ COPY --chown=user . /app
15
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI application for API Blackbox.
3
+ """
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.responses import JSONResponse
7
+
8
+ from routers.procesador import router as procesador_router
9
+
10
+ app = FastAPI(
11
+ title="API Blackbox",
12
+ description="Blackbox API service",
13
+ version="1.0.0",
14
+ )
15
+
16
+ app.include_router(procesador_router)
17
+
18
+
19
+ @app.get("/health")
20
+ async def health_check():
21
+ """
22
+ Health check endpoint.
23
+
24
+ Returns:
25
+ dict: Status of the application
26
+ """
27
+ return JSONResponse(
28
+ status_code=200,
29
+ content={
30
+ "status": "healthy",
31
+ "message": "API is running"
32
+ }
33
+ )
34
+
35
+
36
+ @app.get("/")
37
+ async def root():
38
+ """
39
+ Root endpoint.
40
+
41
+ Returns:
42
+ dict: Welcome message
43
+ """
44
+ return {
45
+ "message": "Welcome to API Blackbox",
46
+ "docs": "/docs",
47
+ "redoc": "/redoc"
48
+ }
49
+
50
+
51
+ if __name__ == "__main__":
52
+ import uvicorn
53
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ pydantic
4
+ pydantic-settings
5
+ fastapi[standard]
6
+ httpx
routers/procesador.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+
4
+ import httpx
5
+ from fastapi import APIRouter, File, Form, HTTPException, UploadFile
6
+ from fastapi.responses import Response
7
+
8
+ from schemas import ProcessorManifest
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ def _resolve_url(url: str, path_params: dict[str, str] | None) -> str:
14
+ """Replace {placeholder} in URL with path_params values."""
15
+ if not path_params:
16
+ return url
17
+ for key, value in path_params.items():
18
+ url = url.replace(f"{{{key}}}", str(value))
19
+ return url
20
+
21
+
22
+ def _resolve_files(
23
+ manifest_files: list | None, files_map: dict[str, bytes]
24
+ ) -> dict[str, tuple[str, bytes]]:
25
+ """Build a dict of field_name -> (filename, bytes) from manifest files list."""
26
+ if not manifest_files:
27
+ return {}
28
+ resolved = {}
29
+ for f in manifest_files:
30
+ if f.filename not in files_map:
31
+ raise HTTPException(
32
+ status_code=400,
33
+ detail=f"File '{f.filename}' referenced in manifest but not uploaded in items",
34
+ )
35
+ resolved[f.field_name] = (f.filename, files_map[f.filename])
36
+ return resolved
37
+
38
+
39
+ def _build_json_body(body: dict, file_refs: dict[str, tuple[str, bytes]]) -> dict:
40
+ """Build a JSON body. File fields get base64-encoded."""
41
+ result = dict(body)
42
+ for field_name, (filename, file_bytes) in file_refs.items():
43
+ result[field_name] = base64.b64encode(file_bytes).decode("utf-8")
44
+ return result
45
+
46
+
47
+ def _build_form_body(body: dict, file_refs: dict[str, tuple[str, bytes]]) -> dict[str, str]:
48
+ """Build a form-urlencoded body (text fields only)."""
49
+ if file_refs:
50
+ raise HTTPException(
51
+ status_code=400,
52
+ detail="body_type 'form' does not support file fields. Use 'multipart' or 'json'.",
53
+ )
54
+ return {k: str(v) for k, v in body.items()}
55
+
56
+
57
+ def _build_multipart(
58
+ body: dict, file_refs: dict[str, tuple[str, bytes]]
59
+ ) -> tuple[dict[str, str], list[tuple[str, tuple[str, bytes]]]]:
60
+ """Build multipart data and files lists."""
61
+ data = {k: str(v) for k, v in body.items()}
62
+ files = []
63
+ for field_name, (filename, file_bytes) in file_refs.items():
64
+ data.pop(field_name, None)
65
+ files.append((field_name, (filename, file_bytes)))
66
+ return data, files
67
+
68
+
69
+ @router.post("/procesador")
70
+ async def procesador(
71
+ manifest: str = Form(..., description="JSON manifest with url, method, headers, body_type, body, etc."),
72
+ items: list[UploadFile] = File(default=[], description="Optional files referenced in manifest.files"),
73
+ ):
74
+ # Parse and validate manifest
75
+ try:
76
+ manifest_data = json.loads(manifest)
77
+ except json.JSONDecodeError as e:
78
+ raise HTTPException(status_code=400, detail=f"Invalid JSON in manifest: {e}")
79
+
80
+ try:
81
+ m = ProcessorManifest(**manifest_data)
82
+ except Exception as e:
83
+ raise HTTPException(status_code=400, detail=f"Invalid manifest: {e}")
84
+
85
+ if m.body_type not in ("json", "form", "multipart"):
86
+ raise HTTPException(
87
+ status_code=400,
88
+ detail=f"Invalid body_type '{m.body_type}'. Must be 'json', 'form', or 'multipart'.",
89
+ )
90
+
91
+ # Read uploaded files into memory: filename -> bytes
92
+ files_map: dict[str, bytes] = {}
93
+ for item in items:
94
+ content = await item.read()
95
+ files_map[item.filename] = content
96
+
97
+ # Resolve path params in URL
98
+ url = _resolve_url(m.url, m.path_params)
99
+
100
+ # Resolve file references
101
+ file_refs = _resolve_files(m.files, files_map)
102
+
103
+ # Build the outgoing request
104
+ method = m.method.upper()
105
+ headers = m.headers or {}
106
+ params = m.query_params or {}
107
+ body = m.body or {}
108
+
109
+ request_kwargs: dict = {
110
+ "method": method,
111
+ "url": url,
112
+ "headers": headers,
113
+ "params": params,
114
+ }
115
+
116
+ if body or file_refs:
117
+ if m.body_type == "json":
118
+ request_kwargs["json"] = _build_json_body(body, file_refs)
119
+ elif m.body_type == "form":
120
+ request_kwargs["data"] = _build_form_body(body, file_refs)
121
+ elif m.body_type == "multipart":
122
+ data, files = _build_multipart(body, file_refs)
123
+ request_kwargs["data"] = data
124
+ if files:
125
+ request_kwargs["files"] = files
126
+
127
+ # Execute the request to the target API
128
+ try:
129
+ async with httpx.AsyncClient(timeout=60.0) as client:
130
+ response = await client.request(**request_kwargs)
131
+ except httpx.RequestError as e:
132
+ raise HTTPException(
133
+ status_code=502,
134
+ detail=f"Error calling target API: {e}",
135
+ )
136
+
137
+ # Return the target API's response
138
+ return Response(
139
+ content=response.content,
140
+ status_code=response.status_code,
141
+ headers=dict(response.headers),
142
+ media_type=response.headers.get("content-type"),
143
+ )
schemas.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class ManifestParameter(BaseModel):
6
+ name: str
7
+ location: str
8
+ type: str
9
+ required: bool = False
10
+ description: Optional[str] = None
11
+ default: Optional[Any] = None
12
+ resolved_value: Optional[Any] = None
13
+
14
+
15
+ class ManifestFile(BaseModel):
16
+ field_name: str
17
+ filename: str
18
+
19
+
20
+ class ProcessorManifest(BaseModel):
21
+ api_name: Optional[str] = None
22
+ api_description: Optional[str] = None
23
+ url: str
24
+ method: str = "POST"
25
+ body_type: str = "json" # "json" | "form" | "multipart"
26
+ headers: Optional[dict[str, str]] = None
27
+ parameters: Optional[list[ManifestParameter]] = None
28
+ body: Optional[dict[str, Any]] = None
29
+ query_params: Optional[dict[str, str]] = None
30
+ path_params: Optional[dict[str, str]] = None
31
+ files: Optional[list[ManifestFile]] = None