opendataloader-pdf 0.0.0__py3-none-any.whl → 1.8.2__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.
@@ -1,5 +1,5 @@
1
1
 
2
- Copyright © 2025 Hancom, Inc.
2
+ Copyright © 2025-2026 Hancom, Inc.
3
3
 
4
4
  This Source Code Form is subject to the terms of the Mozilla Public License 2.0 (MPL-2.0).
5
5
  If a copy of the MPL was not distributed with this file, you can obtain one at https://mozilla.org/MPL/2.0/.
@@ -1,3 +1,3 @@
1
- from .wrapper import run
1
+ from .wrapper import run, convert, run_jar
2
2
 
3
- __all__ = ["run"]
3
+ __all__ = ["run", "convert", "run_jar"]
@@ -0,0 +1,5 @@
1
+ from .wrapper import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,236 @@
1
+ # AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY
2
+ # Run `npm run generate-options` to regenerate
3
+
4
+ """
5
+ CLI option definitions for opendataloader-pdf.
6
+ """
7
+ from typing import Any, Dict, List
8
+
9
+
10
+ # Option metadata list
11
+ CLI_OPTIONS: List[Dict[str, Any]] = [
12
+ {
13
+ "name": "output-dir",
14
+ "python_name": "output_dir",
15
+ "short_name": "o",
16
+ "type": "string",
17
+ "required": False,
18
+ "default": None,
19
+ "description": "Directory where output files are written. Default: input file directory",
20
+ },
21
+ {
22
+ "name": "password",
23
+ "python_name": "password",
24
+ "short_name": "p",
25
+ "type": "string",
26
+ "required": False,
27
+ "default": None,
28
+ "description": "Password for encrypted PDF files",
29
+ },
30
+ {
31
+ "name": "format",
32
+ "python_name": "format",
33
+ "short_name": "f",
34
+ "type": "string",
35
+ "required": False,
36
+ "default": None,
37
+ "description": "Output formats (comma-separated). Values: json, text, html, pdf, markdown, markdown-with-html, markdown-with-images. Default: json",
38
+ },
39
+ {
40
+ "name": "quiet",
41
+ "python_name": "quiet",
42
+ "short_name": "q",
43
+ "type": "boolean",
44
+ "required": False,
45
+ "default": False,
46
+ "description": "Suppress console logging output",
47
+ },
48
+ {
49
+ "name": "content-safety-off",
50
+ "python_name": "content_safety_off",
51
+ "short_name": None,
52
+ "type": "string",
53
+ "required": False,
54
+ "default": None,
55
+ "description": "Disable content safety filters. Values: all, hidden-text, off-page, tiny, hidden-ocg",
56
+ },
57
+ {
58
+ "name": "keep-line-breaks",
59
+ "python_name": "keep_line_breaks",
60
+ "short_name": None,
61
+ "type": "boolean",
62
+ "required": False,
63
+ "default": False,
64
+ "description": "Preserve original line breaks in extracted text",
65
+ },
66
+ {
67
+ "name": "replace-invalid-chars",
68
+ "python_name": "replace_invalid_chars",
69
+ "short_name": None,
70
+ "type": "string",
71
+ "required": False,
72
+ "default": " ",
73
+ "description": "Replacement character for invalid/unrecognized characters. Default: space",
74
+ },
75
+ {
76
+ "name": "use-struct-tree",
77
+ "python_name": "use_struct_tree",
78
+ "short_name": None,
79
+ "type": "boolean",
80
+ "required": False,
81
+ "default": False,
82
+ "description": "Use PDF structure tree (tagged PDF) for reading order and semantic structure",
83
+ },
84
+ {
85
+ "name": "table-method",
86
+ "python_name": "table_method",
87
+ "short_name": None,
88
+ "type": "string",
89
+ "required": False,
90
+ "default": "default",
91
+ "description": "Table detection method. Values: default (border-based), cluster (border + cluster). Default: default",
92
+ },
93
+ {
94
+ "name": "reading-order",
95
+ "python_name": "reading_order",
96
+ "short_name": None,
97
+ "type": "string",
98
+ "required": False,
99
+ "default": "xycut",
100
+ "description": "Reading order algorithm. Values: off, xycut. Default: xycut",
101
+ },
102
+ {
103
+ "name": "markdown-page-separator",
104
+ "python_name": "markdown_page_separator",
105
+ "short_name": None,
106
+ "type": "string",
107
+ "required": False,
108
+ "default": None,
109
+ "description": "Separator between pages in Markdown output. Use %%page-number%% for page numbers. Default: none",
110
+ },
111
+ {
112
+ "name": "text-page-separator",
113
+ "python_name": "text_page_separator",
114
+ "short_name": None,
115
+ "type": "string",
116
+ "required": False,
117
+ "default": None,
118
+ "description": "Separator between pages in text output. Use %%page-number%% for page numbers. Default: none",
119
+ },
120
+ {
121
+ "name": "html-page-separator",
122
+ "python_name": "html_page_separator",
123
+ "short_name": None,
124
+ "type": "string",
125
+ "required": False,
126
+ "default": None,
127
+ "description": "Separator between pages in HTML output. Use %%page-number%% for page numbers. Default: none",
128
+ },
129
+ {
130
+ "name": "image-output",
131
+ "python_name": "image_output",
132
+ "short_name": None,
133
+ "type": "string",
134
+ "required": False,
135
+ "default": "external",
136
+ "description": "Image output mode. Values: off (no images), embedded (Base64 data URIs), external (file references). Default: external",
137
+ },
138
+ {
139
+ "name": "image-format",
140
+ "python_name": "image_format",
141
+ "short_name": None,
142
+ "type": "string",
143
+ "required": False,
144
+ "default": "png",
145
+ "description": "Output format for extracted images. Values: png, jpeg. Default: png",
146
+ },
147
+ {
148
+ "name": "image-dir",
149
+ "python_name": "image_dir",
150
+ "short_name": None,
151
+ "type": "string",
152
+ "required": False,
153
+ "default": None,
154
+ "description": "Directory for extracted images",
155
+ },
156
+ {
157
+ "name": "pages",
158
+ "python_name": "pages",
159
+ "short_name": None,
160
+ "type": "string",
161
+ "required": False,
162
+ "default": None,
163
+ "description": "Pages to extract (e.g., \"1,3,5-7\"). Default: all pages",
164
+ },
165
+ {
166
+ "name": "hybrid",
167
+ "python_name": "hybrid",
168
+ "short_name": None,
169
+ "type": "string",
170
+ "required": False,
171
+ "default": "off",
172
+ "description": "Hybrid backend for AI processing. Values: off (default), docling (docling-fast is deprecated alias)",
173
+ },
174
+ {
175
+ "name": "hybrid-mode",
176
+ "python_name": "hybrid_mode",
177
+ "short_name": None,
178
+ "type": "string",
179
+ "required": False,
180
+ "default": "auto",
181
+ "description": "Hybrid triage mode. Values: auto (default, dynamic triage), full (skip triage, all pages to backend)",
182
+ },
183
+ {
184
+ "name": "hybrid-ocr",
185
+ "python_name": "hybrid_ocr",
186
+ "short_name": None,
187
+ "type": "string",
188
+ "required": False,
189
+ "default": "auto",
190
+ "description": "Hybrid OCR mode for Docling backend. Values: auto (default, OCR only where needed), force (force full-page OCR)",
191
+ },
192
+ {
193
+ "name": "hybrid-url",
194
+ "python_name": "hybrid_url",
195
+ "short_name": None,
196
+ "type": "string",
197
+ "required": False,
198
+ "default": None,
199
+ "description": "Hybrid backend server URL (overrides default)",
200
+ },
201
+ {
202
+ "name": "hybrid-timeout",
203
+ "python_name": "hybrid_timeout",
204
+ "short_name": None,
205
+ "type": "string",
206
+ "required": False,
207
+ "default": "30000",
208
+ "description": "Hybrid backend request timeout in milliseconds. Default: 30000",
209
+ },
210
+ {
211
+ "name": "hybrid-fallback",
212
+ "python_name": "hybrid_fallback",
213
+ "short_name": None,
214
+ "type": "boolean",
215
+ "required": False,
216
+ "default": True,
217
+ "description": "Fallback to Java processing on hybrid backend error. Default: true",
218
+ },
219
+ ]
220
+
221
+
222
+ def add_options_to_parser(parser) -> None:
223
+ """Add all CLI options to an argparse.ArgumentParser."""
224
+ for opt in CLI_OPTIONS:
225
+ flags = []
226
+ if opt["short_name"]:
227
+ flags.append(f'-{opt["short_name"]}')
228
+ flags.append(f'--{opt["name"]}')
229
+
230
+ kwargs = {"help": opt["description"]}
231
+ if opt["type"] == "boolean":
232
+ kwargs["action"] = "store_true"
233
+ else:
234
+ kwargs["default"] = None
235
+
236
+ parser.add_argument(*flags, **kwargs)
@@ -0,0 +1,130 @@
1
+ # AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY
2
+ # Run `npm run generate-options` to regenerate
3
+
4
+ """
5
+ Auto-generated convert function for opendataloader-pdf.
6
+ """
7
+ from typing import List, Optional, Union
8
+
9
+ from .runner import run_jar
10
+
11
+
12
+ def convert(
13
+ input_path: Union[str, List[str]],
14
+ output_dir: Optional[str] = None,
15
+ password: Optional[str] = None,
16
+ format: Optional[Union[str, List[str]]] = None,
17
+ quiet: bool = False,
18
+ content_safety_off: Optional[Union[str, List[str]]] = None,
19
+ keep_line_breaks: bool = False,
20
+ replace_invalid_chars: Optional[str] = None,
21
+ use_struct_tree: bool = False,
22
+ table_method: Optional[str] = None,
23
+ reading_order: Optional[str] = None,
24
+ markdown_page_separator: Optional[str] = None,
25
+ text_page_separator: Optional[str] = None,
26
+ html_page_separator: Optional[str] = None,
27
+ image_output: Optional[str] = None,
28
+ image_format: Optional[str] = None,
29
+ image_dir: Optional[str] = None,
30
+ pages: Optional[str] = None,
31
+ hybrid: Optional[str] = None,
32
+ hybrid_mode: Optional[str] = None,
33
+ hybrid_ocr: Optional[str] = None,
34
+ hybrid_url: Optional[str] = None,
35
+ hybrid_timeout: Optional[str] = None,
36
+ hybrid_fallback: bool = True,
37
+ ) -> None:
38
+ """
39
+ Convert PDF(s) into the requested output format(s).
40
+
41
+ Args:
42
+ input_path: One or more input PDF file paths or directories
43
+ output_dir: Directory where output files are written. Default: input file directory
44
+ password: Password for encrypted PDF files
45
+ format: Output formats (comma-separated). Values: json, text, html, pdf, markdown, markdown-with-html, markdown-with-images. Default: json
46
+ quiet: Suppress console logging output
47
+ content_safety_off: Disable content safety filters. Values: all, hidden-text, off-page, tiny, hidden-ocg
48
+ keep_line_breaks: Preserve original line breaks in extracted text
49
+ replace_invalid_chars: Replacement character for invalid/unrecognized characters. Default: space
50
+ use_struct_tree: Use PDF structure tree (tagged PDF) for reading order and semantic structure
51
+ table_method: Table detection method. Values: default (border-based), cluster (border + cluster). Default: default
52
+ reading_order: Reading order algorithm. Values: off, xycut. Default: xycut
53
+ markdown_page_separator: Separator between pages in Markdown output. Use %page-number% for page numbers. Default: none
54
+ text_page_separator: Separator between pages in text output. Use %page-number% for page numbers. Default: none
55
+ html_page_separator: Separator between pages in HTML output. Use %page-number% for page numbers. Default: none
56
+ image_output: Image output mode. Values: off (no images), embedded (Base64 data URIs), external (file references). Default: external
57
+ image_format: Output format for extracted images. Values: png, jpeg. Default: png
58
+ image_dir: Directory for extracted images
59
+ pages: Pages to extract (e.g., "1,3,5-7"). Default: all pages
60
+ hybrid: Hybrid backend for AI processing. Values: off (default), docling (docling-fast is deprecated alias)
61
+ hybrid_mode: Hybrid triage mode. Values: auto (default, dynamic triage), full (skip triage, all pages to backend)
62
+ hybrid_ocr: Hybrid OCR mode for Docling backend. Values: auto (default, OCR only where needed), force (force full-page OCR)
63
+ hybrid_url: Hybrid backend server URL (overrides default)
64
+ hybrid_timeout: Hybrid backend request timeout in milliseconds. Default: 30000
65
+ hybrid_fallback: Fallback to Java processing on hybrid backend error. Default: true
66
+ """
67
+ args: List[str] = []
68
+
69
+ # Build input paths
70
+ if isinstance(input_path, list):
71
+ args.extend(input_path)
72
+ else:
73
+ args.append(input_path)
74
+
75
+ if output_dir:
76
+ args.extend(["--output-dir", output_dir])
77
+ if password:
78
+ args.extend(["--password", password])
79
+ if format:
80
+ if isinstance(format, list):
81
+ if format:
82
+ args.extend(["--format", ",".join(format)])
83
+ else:
84
+ args.extend(["--format", format])
85
+ if quiet:
86
+ args.append("--quiet")
87
+ if content_safety_off:
88
+ if isinstance(content_safety_off, list):
89
+ if content_safety_off:
90
+ args.extend(["--content-safety-off", ",".join(content_safety_off)])
91
+ else:
92
+ args.extend(["--content-safety-off", content_safety_off])
93
+ if keep_line_breaks:
94
+ args.append("--keep-line-breaks")
95
+ if replace_invalid_chars:
96
+ args.extend(["--replace-invalid-chars", replace_invalid_chars])
97
+ if use_struct_tree:
98
+ args.append("--use-struct-tree")
99
+ if table_method:
100
+ args.extend(["--table-method", table_method])
101
+ if reading_order:
102
+ args.extend(["--reading-order", reading_order])
103
+ if markdown_page_separator:
104
+ args.extend(["--markdown-page-separator", markdown_page_separator])
105
+ if text_page_separator:
106
+ args.extend(["--text-page-separator", text_page_separator])
107
+ if html_page_separator:
108
+ args.extend(["--html-page-separator", html_page_separator])
109
+ if image_output:
110
+ args.extend(["--image-output", image_output])
111
+ if image_format:
112
+ args.extend(["--image-format", image_format])
113
+ if image_dir:
114
+ args.extend(["--image-dir", image_dir])
115
+ if pages:
116
+ args.extend(["--pages", pages])
117
+ if hybrid:
118
+ args.extend(["--hybrid", hybrid])
119
+ if hybrid_mode:
120
+ args.extend(["--hybrid-mode", hybrid_mode])
121
+ if hybrid_ocr:
122
+ args.extend(["--hybrid-ocr", hybrid_ocr])
123
+ if hybrid_url:
124
+ args.extend(["--hybrid-url", hybrid_url])
125
+ if hybrid_timeout:
126
+ args.extend(["--hybrid-timeout", hybrid_timeout])
127
+ if hybrid_fallback:
128
+ args.append("--hybrid-fallback")
129
+
130
+ run_jar(args, quiet)
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python3
2
+ """Fast docling server using DocumentConverter singleton.
3
+
4
+ A lightweight FastAPI server optimized for hybrid PDF processing:
5
+ 1. Using DocumentConverter singletons (no per-request initialization)
6
+ 2. Returns only JSON (DoclingDocument format) - markdown/HTML generated by Java
7
+
8
+ Usage:
9
+ opendataloader-pdf-hybrid [--port PORT] [--host HOST]
10
+
11
+ # Default: http://localhost:5002
12
+ opendataloader-pdf-hybrid
13
+
14
+ # Custom port
15
+ opendataloader-pdf-hybrid --port 5003
16
+
17
+ API Endpoints:
18
+ GET /health - Health check
19
+ POST /v1/convert/file - Convert PDF to JSON
20
+
21
+ The /v1/convert/file endpoint parameters:
22
+ - files: PDF file (multipart/form-data)
23
+ - page_ranges: Page range to process (optional)
24
+ - force_ocr: Force full-page OCR mode (optional, default: false)
25
+
26
+ Requirements:
27
+ Install with hybrid extra: pip install opendataloader-pdf[hybrid]
28
+ """
29
+
30
+ import argparse
31
+ import logging
32
+ import os
33
+ import tempfile
34
+ import time
35
+ import traceback
36
+ from contextlib import asynccontextmanager
37
+ from typing import Optional
38
+
39
+ logging.basicConfig(
40
+ level=logging.INFO,
41
+ format="%(asctime)s - %(levelname)s - %(message)s",
42
+ )
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Configuration
46
+ DEFAULT_HOST = "0.0.0.0"
47
+ DEFAULT_PORT = 5002
48
+ MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB max file size
49
+
50
+ # Global converter instances (initialized on startup)
51
+ converter_auto = None # force_full_page_ocr=False
52
+ converter_force = None # force_full_page_ocr=True
53
+
54
+
55
+ def _check_dependencies():
56
+ """Check if hybrid dependencies are installed."""
57
+ missing = []
58
+ try:
59
+ import uvicorn # noqa: F401
60
+ except ImportError:
61
+ missing.append("uvicorn")
62
+ try:
63
+ import fastapi # noqa: F401
64
+ except ImportError:
65
+ missing.append("fastapi")
66
+ try:
67
+ import docling # noqa: F401
68
+ except ImportError:
69
+ missing.append("docling")
70
+
71
+ if missing:
72
+ raise ImportError(
73
+ f"Missing dependencies: {', '.join(missing)}. "
74
+ "Install with: pip install opendataloader-pdf[hybrid]"
75
+ )
76
+
77
+
78
+ def create_converter(force_full_page_ocr: bool = False):
79
+ """Create a DocumentConverter with the specified OCR options.
80
+
81
+ Args:
82
+ force_full_page_ocr: If True, force OCR on all pages regardless of text content.
83
+ If False (default), OCR only where needed.
84
+ """
85
+ from docling.datamodel.base_models import InputFormat
86
+ from docling.datamodel.pipeline_options import (
87
+ EasyOcrOptions,
88
+ PdfPipelineOptions,
89
+ TableFormerMode,
90
+ TableStructureOptions,
91
+ )
92
+ from docling.document_converter import DocumentConverter, PdfFormatOption
93
+
94
+ pipeline_options = PdfPipelineOptions(
95
+ do_ocr=True,
96
+ do_table_structure=True,
97
+ ocr_options=EasyOcrOptions(force_full_page_ocr=force_full_page_ocr),
98
+ table_structure_options=TableStructureOptions(
99
+ mode=TableFormerMode.ACCURATE
100
+ ),
101
+ )
102
+
103
+ return DocumentConverter(
104
+ format_options={
105
+ InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)
106
+ }
107
+ )
108
+
109
+
110
+ def create_app():
111
+ """Create and configure the FastAPI application."""
112
+ from fastapi import FastAPI, File, Form, UploadFile
113
+ from fastapi.responses import JSONResponse
114
+
115
+ @asynccontextmanager
116
+ async def lifespan(_app: FastAPI):
117
+ """Lifespan context manager for startup and shutdown events."""
118
+ global converter_auto, converter_force
119
+ logger.info("Initializing DocumentConverters...")
120
+ start = time.perf_counter()
121
+
122
+ # Initialize both converters at startup
123
+ converter_auto = create_converter(force_full_page_ocr=False)
124
+ converter_force = create_converter(force_full_page_ocr=True)
125
+
126
+ elapsed = time.perf_counter() - start
127
+ logger.info(f"DocumentConverters initialized in {elapsed:.2f}s")
128
+ yield
129
+ # Cleanup on shutdown (if needed)
130
+
131
+ app = FastAPI(
132
+ title="Docling Fast Server",
133
+ description="Fast PDF conversion using docling SDK with singleton pattern",
134
+ version="1.0.0",
135
+ lifespan=lifespan,
136
+ )
137
+
138
+ @app.get("/health")
139
+ def health():
140
+ """Health check endpoint."""
141
+ return {"status": "ok"}
142
+
143
+ @app.post("/v1/convert/file")
144
+ async def convert_file(
145
+ files: UploadFile = File(...),
146
+ page_ranges: Optional[str] = Form(default=None),
147
+ force_ocr: Optional[str] = Form(default=None),
148
+ ):
149
+ """Convert PDF file to JSON (DoclingDocument format).
150
+
151
+ Only JSON output is provided - markdown and HTML are generated by
152
+ Java processors for consistent reading order application.
153
+
154
+ Args:
155
+ files: The PDF file to convert
156
+ page_ranges: Page range string "start-end" (e.g., "1-5") (optional)
157
+ force_ocr: If "true", use force full-page OCR mode (optional)
158
+
159
+ Returns:
160
+ JSON response with document content.
161
+ """
162
+ global converter_auto, converter_force
163
+
164
+ # Select converter based on force_ocr parameter
165
+ use_force_ocr = force_ocr and force_ocr.lower() == "true"
166
+ converter = converter_force if use_force_ocr else converter_auto
167
+
168
+ if converter is None:
169
+ return JSONResponse(
170
+ {"status": "failure", "errors": ["Server not initialized"]},
171
+ status_code=503,
172
+ )
173
+
174
+ # Parse page_ranges string to tuple
175
+ page_range_tuple = None
176
+ if page_ranges:
177
+ try:
178
+ parts = page_ranges.split("-")
179
+ if len(parts) == 2:
180
+ page_range_tuple = (int(parts[0]), int(parts[1]))
181
+ except ValueError:
182
+ pass
183
+
184
+ # Read and validate file size
185
+ content = await files.read()
186
+ if len(content) > MAX_FILE_SIZE:
187
+ return JSONResponse(
188
+ {
189
+ "status": "failure",
190
+ "errors": [f"File size exceeds maximum allowed ({MAX_FILE_SIZE // (1024*1024)}MB)"],
191
+ },
192
+ status_code=413,
193
+ )
194
+
195
+ # Save uploaded file to temp location
196
+ tmp_path = None
197
+ with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
198
+ tmp.write(content)
199
+ tmp_path = tmp.name
200
+
201
+ try:
202
+ start = time.perf_counter()
203
+ if page_range_tuple:
204
+ result = converter.convert(tmp_path, page_range=page_range_tuple)
205
+ else:
206
+ result = converter.convert(tmp_path)
207
+ processing_time = time.perf_counter() - start
208
+
209
+ # Export to JSON (DoclingDocument format)
210
+ json_content = result.document.export_to_dict()
211
+
212
+ # Build response compatible with docling-serve format
213
+ response = {
214
+ "status": "success",
215
+ "document": {
216
+ "json_content": json_content,
217
+ },
218
+ "processing_time": processing_time,
219
+ "force_ocr": use_force_ocr,
220
+ }
221
+
222
+ return JSONResponse(response)
223
+
224
+ except Exception as e:
225
+ logger.error(f"PDF conversion failed: {e}\n{traceback.format_exc()}")
226
+ return JSONResponse(
227
+ {"status": "failure", "errors": ["PDF conversion failed"]},
228
+ status_code=500,
229
+ )
230
+ finally:
231
+ if tmp_path and os.path.exists(tmp_path):
232
+ os.unlink(tmp_path)
233
+
234
+ return app
235
+
236
+
237
+ def main():
238
+ """Run the server."""
239
+ _check_dependencies()
240
+ import uvicorn
241
+
242
+ parser = argparse.ArgumentParser(description="Docling Fast Server for opendataloader-pdf")
243
+ parser.add_argument(
244
+ "--host",
245
+ default=DEFAULT_HOST,
246
+ help=f"Host to bind to (default: {DEFAULT_HOST})",
247
+ )
248
+ parser.add_argument(
249
+ "--port",
250
+ type=int,
251
+ default=DEFAULT_PORT,
252
+ help=f"Port to bind to (default: {DEFAULT_PORT})",
253
+ )
254
+ parser.add_argument(
255
+ "--log-level",
256
+ default="info",
257
+ choices=["debug", "info", "warning", "error"],
258
+ help="Log level (default: info)",
259
+ )
260
+ args = parser.parse_args()
261
+
262
+ logger.info(f"Starting Docling Fast Server on http://{args.host}:{args.port}")
263
+ app = create_app()
264
+ uvicorn.run(
265
+ app,
266
+ host=args.host,
267
+ port=args.port,
268
+ log_level=args.log_level,
269
+ )
270
+
271
+
272
+ if __name__ == "__main__":
273
+ main()