Ark-kun commited on
Commit
3d0c3c0
·
1 Parent(s): e54baa0

chore: Renamed start_HuggingFace.py to start_HuggingFace_single_tenant.py

Browse files
huggingface_overlay/start_HuggingFace.py CHANGED
@@ -1,565 +1,9 @@
1
- import logging
2
  import os
3
- import pathlib
4
- import typing
5
 
6
- import fastapi
7
- import huggingface_hub
8
- import huggingface_hub.errors
9
 
10
- ENABLE_HUGGINGFACE_AUTH = True
11
-
12
-
13
- # region Paths configuration
14
-
15
- root_data_dir: str = "./data/"
16
- root_data_dir_path = pathlib.Path(root_data_dir).resolve()
17
- print(f"{root_data_dir_path=}")
18
-
19
- # artifacts_dir_path = root_data_dir_path / "artifacts"
20
- # logs_dir_path = root_data_dir_path / "logs"
21
-
22
- root_data_dir_path.mkdir(parents=True, exist_ok=True)
23
- # artifacts_dir_path.mkdir(parents=True, exist_ok=True)
24
- # logs_dir_path.mkdir(parents=True, exist_ok=True)
25
- # endregion
26
-
27
- # region: DB Configuration
28
- database_path = root_data_dir_path / "db.sqlite"
29
- database_uri = f"sqlite:///{database_path}"
30
- print(f"{database_uri=}")
31
- # endregion
32
-
33
- # region: Storage configuration
34
- # from cloud_pipelines.orchestration.storage_providers import local_storage
35
- # storage_provider = local_storage.LocalStorageProvider()
36
- from cloud_pipelines_backend.storage_providers import huggingface_repo_storage
37
-
38
- storage_provider = huggingface_repo_storage.HuggingFaceRepoStorageProvider()
39
-
40
- # artifacts_root_uri = artifacts_dir_path.as_posix()
41
- # logs_root_uri = logs_dir_path.as_posix()
42
- artifacts_root_uri = os.environ.get("DATA_DIR_URI")
43
- logs_root_uri = artifacts_root_uri
44
- # endregion
45
-
46
- # region: Authentication configuration
47
- import fastapi
48
-
49
- print(f"{os.environ=}")
50
-
51
- print(f'{os.environ.get("PERSISTENT_STORAGE_ENABLED")=}')
52
-
53
- # user or org name
54
- hf_space_author_name = os.environ.get("SPACE_AUTHOR_NAME")
55
- # Creator *user* ID (never org ID)
56
- hf_space_creator_user_id = os.environ.get("SPACE_CREATOR_USER_ID")
57
- # SPACE_ID="TangleML/tangle" == f"{SPACE_AUTHOR_NAME}/{SPACE_REPO_NAME}"
58
- print(f"{hf_space_author_name=}")
59
- print(f"{hf_space_creator_user_id=}")
60
-
61
- hf_token: str | None = None
62
- try:
63
- hf_token = huggingface_hub.get_token()
64
- except Exception as ex:
65
- logging.error("Error in `huggingface_hub.get_token()`")
66
-
67
- print(f"{(hf_token is not None)=}")
68
-
69
- hf_whoami: dict | None = None
70
- hf_whoami_user_name: str | None = None
71
- try:
72
- hf_whoami = huggingface_hub.whoami()
73
- hf_whoami_user_name = hf_whoami.get("name") if hf_whoami else None
74
- except Exception as ex:
75
- logging.error("Error in `hugginface_hub.whoami()`")
76
-
77
- print(f"{hf_whoami=}")
78
- print(f"{hf_whoami_user_name=}")
79
-
80
-
81
- # ! This function is just a placeholder for user authentication and authorization so that every request has a user name and permissions.
82
- # ! This placeholder function authenticates the user as user with name "admin" and read/write/admin permissions.
83
- # ! In a real multi-user deployment, the `get_user_details` function MUST be replaced with real authentication/authorization based on OAuth or another auth system.
84
- # ADMIN_USER_NAME = "admin"
85
-
86
- # FIX: Set to False by default
87
- # any_user_can_read = os.environ.get("ANY_USER_CAN_READ", "false").lower() == "true"
88
- any_user_can_read = os.environ.get("ANY_USER_CAN_READ", "true").lower() == "true"
89
- print(f"{any_user_can_read=}")
90
-
91
- IS_HUGGINGFACE_SPACE = hf_space_author_name is not None
92
- print(f"{IS_HUGGINGFACE_SPACE=}")
93
-
94
- if IS_HUGGINGFACE_SPACE:
95
- ADMIN_USER_NAME = hf_space_author_name
96
- print(f"{ADMIN_USER_NAME=}")
97
-
98
- default_component_library_owner_username = ADMIN_USER_NAME
99
-
100
- # Single-tenant
101
- # Selecting the tenant. It's the user or arg that host the space.
102
- tenant_name = hf_space_author_name
103
-
104
- # Create artifact repo if it does not exist.
105
- if not artifacts_root_uri:
106
- repo_user: str = tenant_name
107
- if not repo_user:
108
- raise ValueError("artifacts_root_uri, tenant_name are None")
109
-
110
- repo_type = "dataset"
111
- # dataset_repo_id = f"{repo_user}/{repo_name}"
112
- # SPACE_ID == "TangleML/tangle" == f"{SPACE_AUTHOR_NAME}/{SPACE_REPO_NAME}"
113
- space_repo_id = os.environ["SPACE_ID"]
114
- artifacts_repo_id = space_repo_id + "_data"
115
- # proposed_artifacts_root_uri = f"hf://{repo_type}s/{repo_user}/{repo_name}/data"
116
- proposed_artifacts_root_uri = f"hf://{repo_type}s/{artifacts_repo_id}/data"
117
- print(
118
- f"Artifact repo is not specified. Checking or creating it. {artifacts_repo_id=}"
119
- )
120
- repo_exists = False
121
- try:
122
- _ = huggingface_hub.repo_info(
123
- repo_id=artifacts_repo_id,
124
- repo_type=repo_type,
125
- )
126
- repo_exists = True
127
- print(f"Artifact repo exists: {artifacts_repo_id}")
128
-
129
- except huggingface_hub.errors.RepositoryNotFoundError:
130
- pass
131
- except Exception as ex:
132
- raise RuntimeError(
133
- f"Error checking for the artifacts repo existence. {artifacts_repo_id=}"
134
- ) from ex
135
- if not repo_exists:
136
- print(f"Artifact repo does not exist. Creating it: {artifacts_repo_id}")
137
- try:
138
- _ = huggingface_hub.create_repo(
139
- repo_id=artifacts_repo_id,
140
- repo_type=repo_type,
141
- private=True,
142
- exist_ok=True,
143
- )
144
- except Exception as ex:
145
- raise RuntimeError(
146
- f"Error creating the artifacts repo. {artifacts_repo_id=}"
147
- ) from ex
148
- artifacts_root_uri = proposed_artifacts_root_uri
149
- logs_root_uri = artifacts_root_uri
150
-
151
- print(f"{artifacts_root_uri=}")
152
-
153
- # We need to be careful and prevent public spaces with HF_TOKEN set from letting anyone exploit the HF_TOKEN user.
154
- def get_user_details(request: fastapi.Request):
155
- user_can_read = False
156
- user_can_write = False
157
- user_can_admin = False
158
- user_can_read = user_can_read or any_user_can_read
159
-
160
- oauth_info = huggingface_hub.parse_huggingface_oauth(request)
161
- # if "USER_PERMISSIONS_MAP" in os.environ:
162
- # ...
163
-
164
- if oauth_info:
165
- logger.info(f"{oauth_info=}")
166
- logger.info(f"{oauth_info.user_info=}")
167
- logger.info(f"{oauth_info.user_info.is_pro=}")
168
- logger.info(f"{oauth_info.user_info.can_pay=}")
169
- # TODO: Allow access for users belonging to an allowed org
170
-
171
- user_is_space_author = (
172
- oauth_info.user_info.preferred_username == hf_space_author_name
173
- )
174
- user_is_space_author_by_id = (
175
- oauth_info.user_info.sub == hf_space_creator_user_id
176
- )
177
- # oauth_info.user_info.orgs[0].role_in_org
178
- user_belongs_to_space_org = any(
179
- org.preferred_username == hf_space_author_name
180
- for org in oauth_info.user_info.orgs or []
181
- )
182
- logger.info(f"{user_belongs_to_space_org=}")
183
- logger.info(f"{user_is_space_author=}")
184
- logger.info(f"{user_is_space_author_by_id=}")
185
-
186
- user_can_write = user_can_write or user_is_space_author
187
- user_can_admin = user_can_admin or user_is_space_author
188
-
189
- try:
190
- # Checking user's role in the space org:
191
- # For some reason, in OAuth_info, orgs are always empty.
192
- # Getting the info using whoami
193
- # This leads to extra HF API requests. Find a better way to fix.
194
- logger.info(f"{huggingface_hub.whoami(token=oauth_info.access_token)=}")
195
- oauth_whoami_user_info = huggingface_hub.whoami(
196
- token=oauth_info.access_token
197
- )
198
- user_orgs = oauth_whoami_user_info.get("orgs", [])
199
- space_org_candidates = [
200
- user_org
201
- for user_org in user_orgs
202
- # Does not work: hf_space_creator_user_id is the creator user ID, not the space org ID
203
- # if user_org.get("id") == hf_space_creator_user_id
204
- if user_org.get("name") == hf_space_author_name
205
- ]
206
- if space_org_candidates:
207
- space_org = space_org_candidates[0]
208
- logger.info(f"{space_org=}")
209
- user_role_in_org = space_org.get("roleInOrg")
210
- logger.info(f"{user_role_in_org=}")
211
-
212
- if user_role_in_org == "admin":
213
- user_can_read = True
214
- user_can_write = True
215
- user_can_admin = True
216
- elif user_role_in_org in ("write", "contribute"):
217
- user_can_read = True
218
- user_can_write = True
219
- elif user_role_in_org == "read":
220
- user_can_read = True
221
- else:
222
- pass
223
-
224
- user_details = api_router.UserDetails(
225
- name=oauth_info.user_info.preferred_username,
226
- permissions=api_router.Permissions(
227
- read=user_can_read,
228
- write=user_can_write,
229
- admin=user_can_admin,
230
- ),
231
- # oauth_user_info = request.session["oauth_info"].get("userinfo", {}),
232
- )
233
- logger.info(f"{user_details=}")
234
- return user_details
235
- except huggingface_hub.errors.HfHubHTTPError as ex:
236
- # Maybe redirect to logout or login API?
237
- # Does not work. The browser is not redirected
238
- # logger.error(
239
- # f"Error getting authentication info from HuggingFace. Redirecting to login",
240
- # exc_info=True,
241
- # )
242
- # raise fastapi.HTTPException(
243
- # status_code=302,
244
- # detail="Authorization error",
245
- # # headers={"Location": "/api/oauth/huggingface/logout"},
246
- # headers={"Location": "/api/oauth/huggingface/login"},
247
- # )
248
- if ex.response and ex.response.status_code == 401:
249
- logger.error(
250
- f"Error getting authentication info from HuggingFace. Deleting session OAuth info",
251
- exc_info=True,
252
- )
253
- request.session.pop("oauth_info", None)
254
- else:
255
- logger.error(
256
- f"Error getting authentication info from HuggingFace.",
257
- exc_info=True,
258
- )
259
- # Return unauthenticated
260
- return api_router.UserDetails(
261
- # name="anonymous",
262
- name=None,
263
- permissions=api_router.Permissions(
264
- read=any_user_can_read,
265
- write=False,
266
- admin=False,
267
- ),
268
- )
269
- # return None
270
- # We cannot raise error here. /api/pipeline_runs/ route depends on get_user_name
271
- # raise fastapi.HTTPException(
272
- # status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
273
- # detail="Unauthenticated",
274
- # )
275
-
276
- else:
277
- # We're not in space.
278
- if not artifacts_root_uri:
279
- raise ValueError("Must provide artifacts repo root URI")
280
-
281
- ADMIN_USER_NAME = hf_whoami_user_name or "admin"
282
- print(f"{ADMIN_USER_NAME=}")
283
-
284
- default_component_library_owner_username = ADMIN_USER_NAME
285
-
286
- # We need to be careful and prevent public spaces with HF_TOKEN set from letting anyone exploit the HF_TOKEN user.
287
- def get_user_details(request: fastapi.Request):
288
- return api_router.UserDetails(
289
- name=ADMIN_USER_NAME,
290
- permissions=api_router.Permissions(
291
- read=True,
292
- write=True,
293
- admin=True,
294
- ),
295
- )
296
-
297
-
298
- # !!! TODO: Use authenticated user's token to run Jobs via launcher.
299
-
300
- # endregion
301
-
302
-
303
- # region: Logging configuration
304
- import logging.config
305
-
306
- LOGGING_CONFIG = {
307
- "version": 1,
308
- "disable_existing_loggers": True,
309
- "formatters": {
310
- "standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"},
311
- },
312
- "handlers": {
313
- "default": {
314
- # "level": "INFO",
315
- "level": "DEBUG",
316
- "formatter": "standard",
317
- "class": "logging.StreamHandler",
318
- "stream": "ext://sys.stderr",
319
- },
320
- },
321
- "loggers": {
322
- # root logger
323
- "": {
324
- "level": "INFO",
325
- "handlers": ["default"],
326
- "propagate": False,
327
- },
328
- __name__: {
329
- "level": "DEBUG",
330
- "handlers": ["default"],
331
- "propagate": False,
332
- },
333
- "cloud_pipelines_backend.orchestrator_sql": {
334
- "level": "DEBUG",
335
- "handlers": ["default"],
336
- "propagate": False,
337
- },
338
- "cloud_pipelines_backend.launchers.huggingface_launchers": {
339
- "level": "DEBUG",
340
- "handlers": ["default"],
341
- "propagate": False,
342
- },
343
- "cloud_pipelines.orchestration.launchers.local_docker_launchers": {
344
- "level": "DEBUG",
345
- "handlers": ["default"],
346
- "propagate": False,
347
- },
348
- "uvicorn.error": {
349
- "level": "DEBUG",
350
- "handlers": ["default"],
351
- # Fix triplicated log messages
352
- "propagate": False,
353
- },
354
- "uvicorn.access": {
355
- "level": "DEBUG",
356
- "handlers": ["default"],
357
- },
358
- "watchfiles.main": {
359
- "level": "WARNING",
360
- "handlers": ["default"],
361
- },
362
- },
363
- }
364
-
365
- logging.config.dictConfig(LOGGING_CONFIG)
366
-
367
- logger = logging.getLogger(__name__)
368
- # endregion
369
-
370
- # region: Database engine initialization
371
- from cloud_pipelines_backend import database_ops
372
-
373
- db_engine = database_ops.create_db_engine(
374
- database_uri=database_uri,
375
- )
376
- # endregion
377
-
378
-
379
- # region: Launcher configuration
380
- from cloud_pipelines_backend.launchers import huggingface_launchers
381
-
382
- # Requires HF_TOKEN
383
- launcher = huggingface_launchers.HuggingFaceJobsContainerLauncher(
384
- namespace=hf_space_author_name
385
- )
386
- # endregion
387
-
388
-
389
- # region: Orchestrator configuration
390
- default_task_annotations = {}
391
- sleep_seconds_between_queue_sweeps: float = 5.0
392
- # endregion
393
-
394
-
395
- # region: Orchestrator initialization
396
-
397
- import logging
398
- import pathlib
399
-
400
- import sqlalchemy
401
- from sqlalchemy import orm
402
-
403
- from cloud_pipelines.orchestration.storage_providers import (
404
- interfaces as storage_interfaces,
405
- )
406
- from cloud_pipelines_backend import orchestrator_sql
407
-
408
-
409
- def run_orchestrator(
410
- db_engine: sqlalchemy.Engine,
411
- storage_provider: storage_interfaces.StorageProvider,
412
- data_root_uri: str,
413
- logs_root_uri: str,
414
- sleep_seconds_between_queue_sweeps: float = 5.0,
415
- ):
416
- # logger = logging.getLogger(__name__)
417
- # orchestrator_logger = logging.getLogger("cloud_pipelines_backend.orchestrator_sql")
418
-
419
- # orchestrator_logger.setLevel(logging.DEBUG)
420
- # formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s")
421
-
422
- # stderr_handler = logging.StreamHandler()
423
- # stderr_handler.setLevel(logging.INFO)
424
- # stderr_handler.setFormatter(formatter)
425
-
426
- # # TODO: Disable the default logger instead of not adding a new one
427
- # # orchestrator_logger.addHandler(stderr_handler)
428
- # logger.addHandler(stderr_handler)
429
-
430
- logger.info("Starting the orchestrator")
431
-
432
- # With autobegin=False you always need to begin a transaction, even to query the DB.
433
- session_factory = orm.sessionmaker(
434
- autocommit=False, autoflush=False, bind=db_engine
435
- )
436
-
437
- orchestrator = orchestrator_sql.OrchestratorService_Sql(
438
- session_factory=session_factory,
439
- launcher=launcher,
440
- storage_provider=storage_provider,
441
- data_root_uri=data_root_uri,
442
- logs_root_uri=logs_root_uri,
443
- default_task_annotations=default_task_annotations,
444
- sleep_seconds_between_queue_sweeps=sleep_seconds_between_queue_sweeps,
445
- )
446
- orchestrator.run_loop()
447
-
448
-
449
- run_configured_orchestrator = lambda: run_orchestrator(
450
- db_engine=db_engine,
451
- storage_provider=storage_provider,
452
- data_root_uri=artifacts_root_uri,
453
- logs_root_uri=logs_root_uri,
454
- sleep_seconds_between_queue_sweeps=sleep_seconds_between_queue_sweeps,
455
- )
456
- # endregion
457
-
458
-
459
- # region: API Server initialization
460
- import contextlib
461
- import threading
462
- import traceback
463
-
464
- import fastapi
465
- from fastapi import staticfiles
466
-
467
- from cloud_pipelines_backend import api_router
468
- from cloud_pipelines_backend import database_ops
469
-
470
-
471
- @contextlib.asynccontextmanager
472
- async def lifespan(app: fastapi.FastAPI):
473
- database_ops.initialize_and_migrate_db(db_engine=db_engine)
474
- threading.Thread(
475
- target=run_configured_orchestrator,
476
- daemon=True,
477
- ).start()
478
- yield
479
-
480
-
481
- app = fastapi.FastAPI(
482
- title="Cloud Pipelines API",
483
- version="0.0.1",
484
- separate_input_output_schemas=False,
485
- lifespan=lifespan,
486
- )
487
-
488
-
489
- @app.exception_handler(Exception)
490
- def handle_error(request: fastapi.Request, exc: BaseException):
491
- exception_str = traceback.format_exception(type(exc), exc, exc.__traceback__)
492
- return fastapi.responses.JSONResponse(
493
- status_code=503,
494
- content={"exception": exception_str},
495
- )
496
-
497
-
498
- api_router.setup_routes(
499
- app=app,
500
- db_engine=db_engine,
501
- user_details_getter=get_user_details,
502
- container_launcher_for_log_streaming=launcher,
503
- default_component_library_owner_username=default_component_library_owner_username,
504
- )
505
-
506
-
507
- # Health check needed by the Web app
508
- @app.get("/services/ping")
509
- def health_check():
510
- return {}
511
-
512
-
513
- # @app.get("/api/users/me")
514
- # def get_current_user(
515
- # user_details: typing.Annotated[
516
- # api_router.UserDetails | None, fastapi.Depends(get_user_details)
517
- # ],
518
- # ) -> api_router.UserDetails | None:
519
- # return user_details
520
-
521
-
522
- # Setting up HuggingFace auth.
523
- # if "HF_TOKEN" in os.environ:
524
-
525
- if ENABLE_HUGGINGFACE_AUTH:
526
- if "OAUTH_CLIENT_SECRET" not in os.environ:
527
- logger.warning(
528
- "HuggingFace auth is enabled, but OAUTH_CLIENT_SECRET env variable is is missing."
529
- )
530
- huggingface_hub.attach_huggingface_oauth(app, route_prefix="/api/")
531
-
532
-
533
- # Mounting the web app if the files exist
534
- this_dir = pathlib.Path(__file__).parent
535
- web_app_search_dirs = [
536
- this_dir / ".." / "pipeline-studio-app" / "build",
537
- this_dir / ".." / "frontend" / "build",
538
- this_dir / ".." / "frontend_build",
539
- this_dir / "pipeline-studio-app" / "build",
540
- ]
541
- found_frontend_build_files = False
542
- for web_app_dir in web_app_search_dirs:
543
- if web_app_dir.exists():
544
- found_frontend_build_files = True
545
- logger.info(
546
- f"Found the Web app static files at {str(web_app_dir)}. Mounting them."
547
- )
548
- # The Web app base URL is currently static and hardcoded.
549
- # TODO: Remove this mount once the base URL becomes relative.
550
- app.mount(
551
- "/pipeline-studio-app/",
552
- staticfiles.StaticFiles(directory=web_app_dir, html=True),
553
- name="static",
554
- )
555
- app.mount(
556
- "/",
557
- staticfiles.StaticFiles(directory=web_app_dir, html=True),
558
- name="static",
559
- )
560
- if not found_frontend_build_files:
561
- logger.warning("The Web app files were not found. Skipping.")
562
- # endregion
563
 
564
  if __name__ == "__main__":
565
  import uvicorn
 
 
1
  import os
 
 
2
 
3
+ __all__ = ["app"]
 
 
4
 
5
+ print("Starting single-tenant mode")
6
+ from start_HuggingFace_single_tenant import app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  if __name__ == "__main__":
9
  import uvicorn
huggingface_overlay/start_HuggingFace_single_tenant.py ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import pathlib
4
+ import typing
5
+
6
+ import fastapi
7
+ import huggingface_hub
8
+ import huggingface_hub.errors
9
+
10
+ ENABLE_HUGGINGFACE_AUTH = True
11
+
12
+
13
+ # region Paths configuration
14
+
15
+ root_data_dir: str = "./data/"
16
+ root_data_dir_path = pathlib.Path(root_data_dir).resolve()
17
+ print(f"{root_data_dir_path=}")
18
+
19
+ # artifacts_dir_path = root_data_dir_path / "artifacts"
20
+ # logs_dir_path = root_data_dir_path / "logs"
21
+
22
+ root_data_dir_path.mkdir(parents=True, exist_ok=True)
23
+ # artifacts_dir_path.mkdir(parents=True, exist_ok=True)
24
+ # logs_dir_path.mkdir(parents=True, exist_ok=True)
25
+ # endregion
26
+
27
+ # region: DB Configuration
28
+ database_path = root_data_dir_path / "db.sqlite"
29
+ database_uri = f"sqlite:///{database_path}"
30
+ print(f"{database_uri=}")
31
+ # endregion
32
+
33
+ # region: Storage configuration
34
+ # from cloud_pipelines.orchestration.storage_providers import local_storage
35
+ # storage_provider = local_storage.LocalStorageProvider()
36
+ from cloud_pipelines_backend.storage_providers import huggingface_repo_storage
37
+
38
+ storage_provider = huggingface_repo_storage.HuggingFaceRepoStorageProvider()
39
+
40
+ # artifacts_root_uri = artifacts_dir_path.as_posix()
41
+ # logs_root_uri = logs_dir_path.as_posix()
42
+ artifacts_root_uri = os.environ.get("DATA_DIR_URI")
43
+ logs_root_uri = artifacts_root_uri
44
+ # endregion
45
+
46
+ # region: Authentication configuration
47
+ import fastapi
48
+
49
+ print(f"{os.environ=}")
50
+
51
+ print(f'{os.environ.get("PERSISTENT_STORAGE_ENABLED")=}')
52
+
53
+ # user or org name
54
+ hf_space_author_name = os.environ.get("SPACE_AUTHOR_NAME")
55
+ # Creator *user* ID (never org ID)
56
+ hf_space_creator_user_id = os.environ.get("SPACE_CREATOR_USER_ID")
57
+ # SPACE_ID="TangleML/tangle" == f"{SPACE_AUTHOR_NAME}/{SPACE_REPO_NAME}"
58
+ print(f"{hf_space_author_name=}")
59
+ print(f"{hf_space_creator_user_id=}")
60
+
61
+ hf_token: str | None = None
62
+ try:
63
+ hf_token = huggingface_hub.get_token()
64
+ except Exception as ex:
65
+ logging.error("Error in `huggingface_hub.get_token()`")
66
+
67
+ print(f"{(hf_token is not None)=}")
68
+
69
+ hf_whoami: dict | None = None
70
+ hf_whoami_user_name: str | None = None
71
+ try:
72
+ hf_whoami = huggingface_hub.whoami()
73
+ hf_whoami_user_name = hf_whoami.get("name") if hf_whoami else None
74
+ except Exception as ex:
75
+ logging.error("Error in `hugginface_hub.whoami()`")
76
+
77
+ print(f"{hf_whoami=}")
78
+ print(f"{hf_whoami_user_name=}")
79
+
80
+
81
+ # ! This function is just a placeholder for user authentication and authorization so that every request has a user name and permissions.
82
+ # ! This placeholder function authenticates the user as user with name "admin" and read/write/admin permissions.
83
+ # ! In a real multi-user deployment, the `get_user_details` function MUST be replaced with real authentication/authorization based on OAuth or another auth system.
84
+ # ADMIN_USER_NAME = "admin"
85
+
86
+ # FIX: Set to False by default
87
+ # any_user_can_read = os.environ.get("ANY_USER_CAN_READ", "false").lower() == "true"
88
+ any_user_can_read = os.environ.get("ANY_USER_CAN_READ", "true").lower() == "true"
89
+ print(f"{any_user_can_read=}")
90
+
91
+ IS_HUGGINGFACE_SPACE = hf_space_author_name is not None
92
+ print(f"{IS_HUGGINGFACE_SPACE=}")
93
+
94
+ if IS_HUGGINGFACE_SPACE:
95
+ ADMIN_USER_NAME = hf_space_author_name
96
+ print(f"{ADMIN_USER_NAME=}")
97
+
98
+ default_component_library_owner_username = ADMIN_USER_NAME
99
+
100
+ # Single-tenant
101
+ # Selecting the tenant. It's the user or arg that host the space.
102
+ tenant_name = hf_space_author_name
103
+
104
+ # Create artifact repo if it does not exist.
105
+ if not artifacts_root_uri:
106
+ repo_user: str = tenant_name
107
+ if not repo_user:
108
+ raise ValueError("artifacts_root_uri, tenant_name are None")
109
+
110
+ repo_type = "dataset"
111
+ # dataset_repo_id = f"{repo_user}/{repo_name}"
112
+ # SPACE_ID == "TangleML/tangle" == f"{SPACE_AUTHOR_NAME}/{SPACE_REPO_NAME}"
113
+ space_repo_id = os.environ["SPACE_ID"]
114
+ artifacts_repo_id = space_repo_id + "_data"
115
+ # proposed_artifacts_root_uri = f"hf://{repo_type}s/{repo_user}/{repo_name}/data"
116
+ proposed_artifacts_root_uri = f"hf://{repo_type}s/{artifacts_repo_id}/data"
117
+ print(
118
+ f"Artifact repo is not specified. Checking or creating it. {artifacts_repo_id=}"
119
+ )
120
+ repo_exists = False
121
+ try:
122
+ _ = huggingface_hub.repo_info(
123
+ repo_id=artifacts_repo_id,
124
+ repo_type=repo_type,
125
+ )
126
+ repo_exists = True
127
+ print(f"Artifact repo exists: {artifacts_repo_id}")
128
+
129
+ except huggingface_hub.errors.RepositoryNotFoundError:
130
+ pass
131
+ except Exception as ex:
132
+ raise RuntimeError(
133
+ f"Error checking for the artifacts repo existence. {artifacts_repo_id=}"
134
+ ) from ex
135
+ if not repo_exists:
136
+ print(f"Artifact repo does not exist. Creating it: {artifacts_repo_id}")
137
+ try:
138
+ _ = huggingface_hub.create_repo(
139
+ repo_id=artifacts_repo_id,
140
+ repo_type=repo_type,
141
+ private=True,
142
+ exist_ok=True,
143
+ )
144
+ except Exception as ex:
145
+ raise RuntimeError(
146
+ f"Error creating the artifacts repo. {artifacts_repo_id=}"
147
+ ) from ex
148
+ artifacts_root_uri = proposed_artifacts_root_uri
149
+ logs_root_uri = artifacts_root_uri
150
+
151
+ print(f"{artifacts_root_uri=}")
152
+
153
+ # We need to be careful and prevent public spaces with HF_TOKEN set from letting anyone exploit the HF_TOKEN user.
154
+ def get_user_details(request: fastapi.Request):
155
+ user_can_read = False
156
+ user_can_write = False
157
+ user_can_admin = False
158
+ user_can_read = user_can_read or any_user_can_read
159
+
160
+ oauth_info = huggingface_hub.parse_huggingface_oauth(request)
161
+ # if "USER_PERMISSIONS_MAP" in os.environ:
162
+ # ...
163
+
164
+ if oauth_info:
165
+ logger.info(f"{oauth_info=}")
166
+ logger.info(f"{oauth_info.user_info=}")
167
+ logger.info(f"{oauth_info.user_info.is_pro=}")
168
+ logger.info(f"{oauth_info.user_info.can_pay=}")
169
+ # TODO: Allow access for users belonging to an allowed org
170
+
171
+ user_is_space_author = (
172
+ oauth_info.user_info.preferred_username == hf_space_author_name
173
+ )
174
+ user_is_space_author_by_id = (
175
+ oauth_info.user_info.sub == hf_space_creator_user_id
176
+ )
177
+ # oauth_info.user_info.orgs[0].role_in_org
178
+ user_belongs_to_space_org = any(
179
+ org.preferred_username == hf_space_author_name
180
+ for org in oauth_info.user_info.orgs or []
181
+ )
182
+ logger.info(f"{user_belongs_to_space_org=}")
183
+ logger.info(f"{user_is_space_author=}")
184
+ logger.info(f"{user_is_space_author_by_id=}")
185
+
186
+ user_can_write = user_can_write or user_is_space_author
187
+ user_can_admin = user_can_admin or user_is_space_author
188
+
189
+ try:
190
+ # Checking user's role in the space org:
191
+ # For some reason, in OAuth_info, orgs are always empty.
192
+ # Getting the info using whoami
193
+ # This leads to extra HF API requests. Find a better way to fix.
194
+ logger.info(f"{huggingface_hub.whoami(token=oauth_info.access_token)=}")
195
+ oauth_whoami_user_info = huggingface_hub.whoami(
196
+ token=oauth_info.access_token
197
+ )
198
+ user_orgs = oauth_whoami_user_info.get("orgs", [])
199
+ space_org_candidates = [
200
+ user_org
201
+ for user_org in user_orgs
202
+ # Does not work: hf_space_creator_user_id is the creator user ID, not the space org ID
203
+ # if user_org.get("id") == hf_space_creator_user_id
204
+ if user_org.get("name") == hf_space_author_name
205
+ ]
206
+ if space_org_candidates:
207
+ space_org = space_org_candidates[0]
208
+ logger.info(f"{space_org=}")
209
+ user_role_in_org = space_org.get("roleInOrg")
210
+ logger.info(f"{user_role_in_org=}")
211
+
212
+ if user_role_in_org == "admin":
213
+ user_can_read = True
214
+ user_can_write = True
215
+ user_can_admin = True
216
+ elif user_role_in_org in ("write", "contribute"):
217
+ user_can_read = True
218
+ user_can_write = True
219
+ elif user_role_in_org == "read":
220
+ user_can_read = True
221
+ else:
222
+ pass
223
+
224
+ user_details = api_router.UserDetails(
225
+ name=oauth_info.user_info.preferred_username,
226
+ permissions=api_router.Permissions(
227
+ read=user_can_read,
228
+ write=user_can_write,
229
+ admin=user_can_admin,
230
+ ),
231
+ # oauth_user_info = request.session["oauth_info"].get("userinfo", {}),
232
+ )
233
+ logger.info(f"{user_details=}")
234
+ return user_details
235
+ except huggingface_hub.errors.HfHubHTTPError as ex:
236
+ # Maybe redirect to logout or login API?
237
+ # Does not work. The browser is not redirected
238
+ # logger.error(
239
+ # f"Error getting authentication info from HuggingFace. Redirecting to login",
240
+ # exc_info=True,
241
+ # )
242
+ # raise fastapi.HTTPException(
243
+ # status_code=302,
244
+ # detail="Authorization error",
245
+ # # headers={"Location": "/api/oauth/huggingface/logout"},
246
+ # headers={"Location": "/api/oauth/huggingface/login"},
247
+ # )
248
+ if ex.response and ex.response.status_code == 401:
249
+ logger.error(
250
+ f"Error getting authentication info from HuggingFace. Deleting session OAuth info",
251
+ exc_info=True,
252
+ )
253
+ request.session.pop("oauth_info", None)
254
+ else:
255
+ logger.error(
256
+ f"Error getting authentication info from HuggingFace.",
257
+ exc_info=True,
258
+ )
259
+ # Return unauthenticated
260
+ return api_router.UserDetails(
261
+ # name="anonymous",
262
+ name=None,
263
+ permissions=api_router.Permissions(
264
+ read=any_user_can_read,
265
+ write=False,
266
+ admin=False,
267
+ ),
268
+ )
269
+ # return None
270
+ # We cannot raise error here. /api/pipeline_runs/ route depends on get_user_name
271
+ # raise fastapi.HTTPException(
272
+ # status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
273
+ # detail="Unauthenticated",
274
+ # )
275
+
276
+ else:
277
+ # We're not in space.
278
+ if not artifacts_root_uri:
279
+ raise ValueError("Must provide artifacts repo root URI")
280
+
281
+ ADMIN_USER_NAME = hf_whoami_user_name or "admin"
282
+ print(f"{ADMIN_USER_NAME=}")
283
+
284
+ default_component_library_owner_username = ADMIN_USER_NAME
285
+
286
+ # We need to be careful and prevent public spaces with HF_TOKEN set from letting anyone exploit the HF_TOKEN user.
287
+ def get_user_details(request: fastapi.Request):
288
+ return api_router.UserDetails(
289
+ name=ADMIN_USER_NAME,
290
+ permissions=api_router.Permissions(
291
+ read=True,
292
+ write=True,
293
+ admin=True,
294
+ ),
295
+ )
296
+
297
+
298
+ # !!! TODO: Use authenticated user's token to run Jobs via launcher.
299
+
300
+ # endregion
301
+
302
+
303
+ # region: Logging configuration
304
+ import logging.config
305
+
306
+ LOGGING_CONFIG = {
307
+ "version": 1,
308
+ "disable_existing_loggers": True,
309
+ "formatters": {
310
+ "standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"},
311
+ },
312
+ "handlers": {
313
+ "default": {
314
+ # "level": "INFO",
315
+ "level": "DEBUG",
316
+ "formatter": "standard",
317
+ "class": "logging.StreamHandler",
318
+ "stream": "ext://sys.stderr",
319
+ },
320
+ },
321
+ "loggers": {
322
+ # root logger
323
+ "": {
324
+ "level": "INFO",
325
+ "handlers": ["default"],
326
+ "propagate": False,
327
+ },
328
+ __name__: {
329
+ "level": "DEBUG",
330
+ "handlers": ["default"],
331
+ "propagate": False,
332
+ },
333
+ "cloud_pipelines_backend.orchestrator_sql": {
334
+ "level": "DEBUG",
335
+ "handlers": ["default"],
336
+ "propagate": False,
337
+ },
338
+ "cloud_pipelines_backend.launchers.huggingface_launchers": {
339
+ "level": "DEBUG",
340
+ "handlers": ["default"],
341
+ "propagate": False,
342
+ },
343
+ "cloud_pipelines.orchestration.launchers.local_docker_launchers": {
344
+ "level": "DEBUG",
345
+ "handlers": ["default"],
346
+ "propagate": False,
347
+ },
348
+ "uvicorn.error": {
349
+ "level": "DEBUG",
350
+ "handlers": ["default"],
351
+ # Fix triplicated log messages
352
+ "propagate": False,
353
+ },
354
+ "uvicorn.access": {
355
+ "level": "DEBUG",
356
+ "handlers": ["default"],
357
+ },
358
+ "watchfiles.main": {
359
+ "level": "WARNING",
360
+ "handlers": ["default"],
361
+ },
362
+ },
363
+ }
364
+
365
+ logging.config.dictConfig(LOGGING_CONFIG)
366
+
367
+ logger = logging.getLogger(__name__)
368
+ # endregion
369
+
370
+ # region: Database engine initialization
371
+ from cloud_pipelines_backend import database_ops
372
+
373
+ db_engine = database_ops.create_db_engine(
374
+ database_uri=database_uri,
375
+ )
376
+ # endregion
377
+
378
+
379
+ # region: Launcher configuration
380
+ from cloud_pipelines_backend.launchers import huggingface_launchers
381
+
382
+ # Requires HF_TOKEN
383
+ launcher = huggingface_launchers.HuggingFaceJobsContainerLauncher(
384
+ namespace=hf_space_author_name
385
+ )
386
+ # endregion
387
+
388
+
389
+ # region: Orchestrator configuration
390
+ default_task_annotations = {}
391
+ sleep_seconds_between_queue_sweeps: float = 5.0
392
+ # endregion
393
+
394
+
395
+ # region: Orchestrator initialization
396
+
397
+ import logging
398
+ import pathlib
399
+
400
+ import sqlalchemy
401
+ from sqlalchemy import orm
402
+
403
+ from cloud_pipelines.orchestration.storage_providers import (
404
+ interfaces as storage_interfaces,
405
+ )
406
+ from cloud_pipelines_backend import orchestrator_sql
407
+
408
+
409
+ def run_orchestrator(
410
+ db_engine: sqlalchemy.Engine,
411
+ storage_provider: storage_interfaces.StorageProvider,
412
+ data_root_uri: str,
413
+ logs_root_uri: str,
414
+ sleep_seconds_between_queue_sweeps: float = 5.0,
415
+ ):
416
+ # logger = logging.getLogger(__name__)
417
+ # orchestrator_logger = logging.getLogger("cloud_pipelines_backend.orchestrator_sql")
418
+
419
+ # orchestrator_logger.setLevel(logging.DEBUG)
420
+ # formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s")
421
+
422
+ # stderr_handler = logging.StreamHandler()
423
+ # stderr_handler.setLevel(logging.INFO)
424
+ # stderr_handler.setFormatter(formatter)
425
+
426
+ # # TODO: Disable the default logger instead of not adding a new one
427
+ # # orchestrator_logger.addHandler(stderr_handler)
428
+ # logger.addHandler(stderr_handler)
429
+
430
+ logger.info("Starting the orchestrator")
431
+
432
+ # With autobegin=False you always need to begin a transaction, even to query the DB.
433
+ session_factory = orm.sessionmaker(
434
+ autocommit=False, autoflush=False, bind=db_engine
435
+ )
436
+
437
+ orchestrator = orchestrator_sql.OrchestratorService_Sql(
438
+ session_factory=session_factory,
439
+ launcher=launcher,
440
+ storage_provider=storage_provider,
441
+ data_root_uri=data_root_uri,
442
+ logs_root_uri=logs_root_uri,
443
+ default_task_annotations=default_task_annotations,
444
+ sleep_seconds_between_queue_sweeps=sleep_seconds_between_queue_sweeps,
445
+ )
446
+ orchestrator.run_loop()
447
+
448
+
449
+ run_configured_orchestrator = lambda: run_orchestrator(
450
+ db_engine=db_engine,
451
+ storage_provider=storage_provider,
452
+ data_root_uri=artifacts_root_uri,
453
+ logs_root_uri=logs_root_uri,
454
+ sleep_seconds_between_queue_sweeps=sleep_seconds_between_queue_sweeps,
455
+ )
456
+ # endregion
457
+
458
+
459
+ # region: API Server initialization
460
+ import contextlib
461
+ import threading
462
+ import traceback
463
+
464
+ import fastapi
465
+ from fastapi import staticfiles
466
+
467
+ from cloud_pipelines_backend import api_router
468
+ from cloud_pipelines_backend import database_ops
469
+
470
+
471
+ @contextlib.asynccontextmanager
472
+ async def lifespan(app: fastapi.FastAPI):
473
+ database_ops.initialize_and_migrate_db(db_engine=db_engine)
474
+ threading.Thread(
475
+ target=run_configured_orchestrator,
476
+ daemon=True,
477
+ ).start()
478
+ yield
479
+
480
+
481
+ app = fastapi.FastAPI(
482
+ title="Cloud Pipelines API",
483
+ version="0.0.1",
484
+ separate_input_output_schemas=False,
485
+ lifespan=lifespan,
486
+ )
487
+
488
+
489
+ @app.exception_handler(Exception)
490
+ def handle_error(request: fastapi.Request, exc: BaseException):
491
+ exception_str = traceback.format_exception(type(exc), exc, exc.__traceback__)
492
+ return fastapi.responses.JSONResponse(
493
+ status_code=503,
494
+ content={"exception": exception_str},
495
+ )
496
+
497
+
498
+ api_router.setup_routes(
499
+ app=app,
500
+ db_engine=db_engine,
501
+ user_details_getter=get_user_details,
502
+ container_launcher_for_log_streaming=launcher,
503
+ default_component_library_owner_username=default_component_library_owner_username,
504
+ )
505
+
506
+
507
+ # Health check needed by the Web app
508
+ @app.get("/services/ping")
509
+ def health_check():
510
+ return {}
511
+
512
+
513
+ # @app.get("/api/users/me")
514
+ # def get_current_user(
515
+ # user_details: typing.Annotated[
516
+ # api_router.UserDetails | None, fastapi.Depends(get_user_details)
517
+ # ],
518
+ # ) -> api_router.UserDetails | None:
519
+ # return user_details
520
+
521
+
522
+ # Setting up HuggingFace auth.
523
+ # if "HF_TOKEN" in os.environ:
524
+
525
+ if ENABLE_HUGGINGFACE_AUTH:
526
+ if "OAUTH_CLIENT_SECRET" not in os.environ:
527
+ logger.warning(
528
+ "HuggingFace auth is enabled, but OAUTH_CLIENT_SECRET env variable is is missing."
529
+ )
530
+ huggingface_hub.attach_huggingface_oauth(app, route_prefix="/api/")
531
+
532
+
533
+ # Mounting the web app if the files exist
534
+ this_dir = pathlib.Path(__file__).parent
535
+ web_app_search_dirs = [
536
+ this_dir / ".." / "pipeline-studio-app" / "build",
537
+ this_dir / ".." / "frontend" / "build",
538
+ this_dir / ".." / "frontend_build",
539
+ this_dir / "pipeline-studio-app" / "build",
540
+ ]
541
+ found_frontend_build_files = False
542
+ for web_app_dir in web_app_search_dirs:
543
+ if web_app_dir.exists():
544
+ found_frontend_build_files = True
545
+ logger.info(
546
+ f"Found the Web app static files at {str(web_app_dir)}. Mounting them."
547
+ )
548
+ # The Web app base URL is currently static and hardcoded.
549
+ # TODO: Remove this mount once the base URL becomes relative.
550
+ app.mount(
551
+ "/pipeline-studio-app/",
552
+ staticfiles.StaticFiles(directory=web_app_dir, html=True),
553
+ name="static",
554
+ )
555
+ app.mount(
556
+ "/",
557
+ staticfiles.StaticFiles(directory=web_app_dir, html=True),
558
+ name="static",
559
+ )
560
+ if not found_frontend_build_files:
561
+ logger.warning("The Web app files were not found. Skipping.")
562
+ # endregion
563
+
564
+ if __name__ == "__main__":
565
+ import uvicorn
566
+
567
+ uvicorn.run(app, host="127.0.0.1", port=8000)